Compare commits

..

67 Commits

Author SHA1 Message Date
Dennis Guse 648ebadb36 Release: v4.9.0 2023-09-26 20:57:59 +02:00
Dennis Guse fda9ef9dd7 Bugfix: TrackList should only mark the currently recording track as recording. 2023-09-26 20:48:11 +02:00
Dennis Guse 2b08eb3287 Cleanup: removed unnecessary getter from data class RecordingStatus. 2023-09-26 08:13:32 +02:00
Leon Marz f4cec0a8b2 Fix broken TrackList UI tests 2023-09-26 07:16:34 +02:00
Dennis Guse b5e88b3e05 Merge pull request #1707 from OpenTracksApp/idleDetection#1187
Idle detection#1187
2023-09-25 18:00:43 +02:00
Dennis Guse 83da4f8f34 Idle: add voice announcement (user configurable).
Fixes of #1174
2023-09-25 17:59:17 +02:00
Dennis Guse 9a50d7bedf Idle: move idle into TrackStatistics. 2023-09-25 17:59:17 +02:00
Dennis Guse 348cb16256 ExportImportTest: extract test for checking recording. 2023-09-25 17:59:17 +02:00
Dennis Guse 7d739e3640 TrackStatisticsUpdater: remove check for max acceleration (was still used for max speed). 2023-09-25 17:59:17 +02:00
Dennis Guse 78d2a0722a Idle: TrackRecordingManager creates IDLE TrackPoints (configurable timeout).
Fixes of #1187.
2023-09-25 17:59:17 +02:00
Dennis Guse 00debe2eb5 Idle: TrackRecordingManager creates IDLE TrackPoints (import/export GPX/KML).
Fixes of #1187.
2023-09-25 17:59:17 +02:00
Dennis Guse 6be9d2477c Idle: TrackRecordingManager creates IDLE TrackPoints.
Fixes of #1187.
2023-09-25 17:59:17 +02:00
Dennis Guse 60fd1cd105 Merge pull request #1704 from OpenTracksApp/action-test
Re-enable Github Action executes tests.
2023-09-23 23:01:00 +02:00
Dennis Guse b360191055 Github action: test runner uses updated dependencies and only executes agains API34. 2023-09-23 23:00:34 +02:00
Dennis Guse 8a20edb83a Revert "Github Actions: disable test execution as results are flaky due to performance limitations."
This reverts commit 8f9cbcc077.
2023-09-23 23:00:34 +02:00
Dennis Guse 165dd401f4 Activity: added scooter (incl. icon).
Fixes #1695.
2023-09-23 21:52:07 +02:00
Dennis Guse 10e65035ee Cleanup: use List.of(). 2023-09-23 21:33:47 +02:00
Dennis Guse f797ce0872 Fix tests due to cf9b432224 2023-09-23 16:46:53 +02:00
Dennis Guse 4425fbae90 Revert "Bump androidx.mediarouter:mediarouter from 1.4.0 to 1.6.0"
This reverts commit f643d86772.
2023-09-23 15:57:03 +02:00
josé m f2c1d08f60 Translated using Weblate (Galician)
Currently translated at 13.4% (16 of 119 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2023-09-21 07:32:04 +02:00
Maksym Movchan 481c77679b Translated using Weblate (Ukrainian)
Currently translated at 97.8% (503 of 514 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2023-09-21 07:32:04 +02:00
Stephan Paternotte 1029460e64 Translated using Weblate (Dutch)
Currently translated at 100.0% (119 of 119 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-09-21 07:32:04 +02:00
Stephan Paternotte b250d95e41 Translated using Weblate (Dutch)
Currently translated at 100.0% (514 of 514 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-09-21 07:32:04 +02:00
josé m bd99daddd4 Translated using Weblate (Galician)
Currently translated at 100.0% (514 of 514 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-09-21 07:32:04 +02:00
Fjuro b3af0b3b5f Translated using Weblate (Czech)
Currently translated at 100.0% (514 of 514 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2023-09-21 07:32:04 +02:00
josé m a151b78da7 Translated using Weblate (Galician)
Currently translated at 11.8% (14 of 118 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2023-09-21 07:32:04 +02:00
Stephan Paternotte 3f760c28da Translated using Weblate (Dutch)
Currently translated at 100.0% (118 of 118 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-09-21 07:32:04 +02:00
Stephan Paternotte 50d20f297e Translated using Weblate (Dutch)
Currently translated at 100.0% (117 of 117 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-09-21 07:32:04 +02:00
dependabot[bot] f643d86772 Bump androidx.mediarouter:mediarouter from 1.4.0 to 1.6.0
Bumps androidx.mediarouter:mediarouter from 1.4.0 to 1.6.0.

---
updated-dependencies:
- dependency-name: androidx.mediarouter:mediarouter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-21 07:31:59 +02:00
Dennis Guse a6f5635cb8 Release: v4.8.4 2023-09-17 10:18:46 +02:00
Dennis Guse bebf89608c Bump com.android.tools.build:gradle from 8.2.0-beta01 to 8.2.0-beta04 2023-09-17 10:17:35 +02:00
Dennis Guse 0cf02082d3 Merge pull request #1687 from lmarz/track
TrackListActivity: Switch to RecyclerView

Fixes #1613
2023-09-14 22:06:00 +02:00
Leon Marz e18017d500 TrackListActivity: Switch to RecyclerView
Part of: #1613
2023-09-14 18:11:02 +02:00
Leon Marz 7776b51f05 TrackListActivity: Set launchMode to singleTop 2023-09-14 18:11:02 +02:00
Leon Marz 30c76de136 Add recycler view adapter for track list 2023-09-14 18:11:02 +02:00
Leon Marz 853469cb28 ContentProviderUtils.java: Add searchTracks method 2023-09-14 18:11:02 +02:00
quaelnix cf9b432224 Add max power stats 2023-09-13 18:38:32 +02:00
dependabot[bot] e130039118 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-07 09:54:38 +02:00
Martin Herrman adb491ee81 Update README.md
Add SportsTracker to file format compatibility table
2023-09-05 22:57:52 +02:00
Dennis Guse 1b295e06da Release: v4.8.3 2023-08-31 22:07:48 +02:00
Dennis Guse 4ca6b230c3 Bugfix: prevent crash due to invalid UUID example (Android 14+ / API34+).
Fixes #1682.
2023-08-31 22:05:31 +02:00
Dennis Guse bb722de756 Upgrade to Gradlew 8.3.0 2023-08-28 20:31:14 +02:00
Dennis Guse 8eab847fae Merge pull request #1672 from lmarz/marker
MarkerListActivity: Switch to RecyclerView
2023-08-28 20:17:38 +02:00
Leon Marz c256cbe07d MarkerListActivity: Switch to RecyclerView
Part of: #1613
2023-08-28 10:02:00 +02:00
Leon Marz 5978886a18 Add recycler view adapter for marker list 2023-08-28 10:02:00 +02:00
Leon Marz 7afa4b86d4 ContentProviderUtils: Add searchMarkers method 2023-08-28 10:01:51 +02:00
Dennis Guse f59ae51c6f Release: v4.8.2 2023-08-27 09:01:20 +02:00
Max Ch 19a5d46e88 Translated using Weblate (Ukrainian)
Currently translated at 97.8% (502 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2023-08-26 19:40:32 +02:00
josé m d2b64391d9 Translated using Weblate (Galician)
Currently translated at 10.3% (12 of 116 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/gl/
2023-08-26 19:40:32 +02:00
Stephan Paternotte fe920d5b5e Translated using Weblate (Dutch)
Currently translated at 100.0% (116 of 116 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-08-26 19:40:32 +02:00
Max Ch d1ba31ff3f Translated using Weblate (Ukrainian)
Currently translated at 93.1% (478 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2023-08-26 19:40:32 +02:00
josé m e7a01b1cd1 Translated using Weblate (Galician)
Currently translated at 100.0% (513 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-08-26 19:40:32 +02:00
Stephan Paternotte 70b65d05c0 Translated using Weblate (Dutch)
Currently translated at 100.0% (115 of 115 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-08-26 19:40:32 +02:00
Max Ch c9ad1b45c6 Translated using Weblate (Ukrainian)
Currently translated at 89.4% (459 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/uk/
2023-08-26 19:40:32 +02:00
ovl-1 4657a9faa0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.2% (499 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nb_NO/
2023-08-26 19:40:32 +02:00
gallegonovato fba6cd7d4a Translated using Weblate (Spanish)
Currently translated at 100.0% (513 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/es/
2023-08-26 19:40:32 +02:00
Tea-NT 970239c614 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (513 of 513 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/zh_Hans/
2023-08-26 19:40:32 +02:00
Peter Storch f83c23e450 show postworkout error messages, #1669 2023-08-26 19:40:27 +02:00
Dennis Guse 326da494eb Java Record for Track.Id and TrackPoint.Id.
Part of #1526.
2023-08-25 23:52:10 +02:00
Dennis Guse fb85f2e976 Bump com.android.tools.build:gradle from 8.1.1 to 8.2.0-beta01 2023-08-25 23:52:10 +02:00
dependabot[bot] 60a34bfcbb Bump org.mockito:mockito-android from 5.4.0 to 5.5.0
Bumps [org.mockito:mockito-android](https://github.com/mockito/mockito) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v5.4.0...v5.5.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-25 23:31:17 +02:00
Dennis Guse 95816937bc Bugfix: request ACCESS_BACKGROUND_LOCATION fails; removing it for now.
Introduced in 3910837d1a

Part of #1653.
2023-08-22 22:06:56 +02:00
Dennis Guse 4f745a94bf Release: v4.8.1 2023-08-22 07:15:53 +02:00
Dennis Guse 9b6df4bf99 Bugfix: request ACCESS_BACKGROUND_LOCATION fails; removing it for now.
Introduced in 3910837d1a

Part of #1653.
2023-08-22 07:15:08 +02:00
dependabot[bot] 73f325e633 Bump com.android.tools.build:gradle from 8.1.0 to 8.1.1
Bumps com.android.tools.build:gradle from 8.1.0 to 8.1.1.

---
updated-dependencies:
- dependency-name: com.android.tools.build:gradle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-22 06:44:06 +02:00
Dennis Guse 28981611f1 GPX export: skip gain/loss if not present.
Part of #1671.
2023-08-20 19:38:37 +02:00
Dennis Guse 7d8c4dcb29 Bugfix: re-introduced race condition fix for newGpsStatus.
Follow-up to e9f24f6bae

Fixes #1668.
2023-08-20 19:28:37 +02:00
136 changed files with 2711 additions and 1737 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
@@ -1,6 +1,3 @@
# We have flaky test results due to performance limitations in Github Actions.
# Happens too often nowadays; See https://github.com/OpenTracksApp/OpenTracks/issues/1409
#https://github.com/marketplace/actions/android-emulator-runner
name: Test
on:
@@ -11,27 +8,20 @@ jobs:
test:
timeout-minutes: 45
runs-on: macos-latest
strategy:
matrix:
api-level: [ 24, 30 ]
target: [ default ]
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
distribution: 'adopt'
java-version: '17'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.26.0
uses: reactivecircus/android-emulator-runner@v2.28.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
api-level: 34
script: ./gradlew connectedCheck
- name: Archive
+8 -8
View File
@@ -125,14 +125,14 @@ For testing via adb: `adb shell am start -e someParameter someValue -n "package/
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
## File formats compatibility with open-source software
| | [GPX 1.1](https://www.topografix.com/GPX/1/1/) | [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) |
|------------------------------------------------------|------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------|
| [OpenLayers 7.1.0](https://openlayers.org/) | ? | [no](https://github.com/openlayers/openlayers/issues/14104) | [no](https://github.com/openlayers/openlayers/issues/14104) |
| [Golden Cheetah 3.5](https://www.goldencheetah.org/) | ? | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) |
| [GpxPod](https://apps.nextcloud.com/apps/gpxpod) | ? | ? | ? |
| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) |
| [FitTrackee](https://github.com/SamR1/FitTrackee) | yes | n/a | n/a |
| | [GPX 1.1](https://www.topografix.com/GPX/1/1/) | [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) |
|-----------------------------------------------------------|--------------------------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------|
| [OpenLayers 7.1.0](https://openlayers.org/) | ? | [no](https://github.com/openlayers/openlayers/issues/14104) | [no](https://github.com/openlayers/openlayers/issues/14104) |
| [Golden Cheetah 3.5](https://www.goldencheetah.org/) | ? | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) |
| [GpxPod](https://apps.nextcloud.com/apps/gpxpod) | ? | ? | ? |
| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) |
| [FitTrackee](https://github.com/SamR1/FitTrackee) | yes | n/a | n/a |
| [SportsTracker](https://github.com/ssaring/sportstracker) | yes, [single tracks only](https://github.com/ssaring/sportstracker/issues/260) | no | no |
## Dashboard API (incl. map)
+4 -4
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath 'com.android.tools.build:gradle:8.2.0-beta04'
}
}
@@ -64,8 +64,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5361
versionName "v4.8.0"
versionCode 5428
versionName "v4.9.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -141,7 +141,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'org.mockito:mockito-android:5.4.0'
androidTestImplementation 'org.mockito:mockito-android:5.5.0'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}
+483
View File
@@ -0,0 +1,483 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22"
height="22"
viewBox="0 0 5.8208333 5.8208333"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="ic_activity.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<inkscape:path-effect
effect="spiro"
id="path-effect3162"
is_visible="true"
lpeversion="1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3149"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.9999999,0,1 @ F,0,0,1,0,2.0000001,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.9999999,0,1 @ F,0,0,1,0,2.0000001,0,1" />
<inkscape:path-effect
hide_knots="false"
only_selected="false"
apply_with_radius="true"
apply_no_radius="true"
use_knot_distance="true"
flexible="false"
chamfer_steps="1"
radius="0"
mode="F"
method="auto"
unit="px"
satellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,4,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.97014234,0,1 @ F,0,0,1,0,0,0,1"
lpeversion="1"
is_visible="true"
id="path-effect2514"
effect="fillet_chamfer"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,4,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.97014234,0,1 @ F,0,0,1,0,0,0,1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect950"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0.51887107,0,1 @ F,0,0,1,0,0.22687835,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,0.51887107,0,1 @ F,0,0,1,0,0.22687835,0,1 @ F,0,0,1,0,0,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0.51887107,0,1 @ F,0,0,1,0,0.22687835,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,0.51887107,0,1 @ F,0,0,1,0,0.22687835,0,1 @ F,0,0,1,0,0,0,1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect946"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0.34892149,0,1 @ F,0,0,1,0,0.18398697,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.34892149,0,1 @ F,0,0,1,0,0.18398697,0,1 @ F,0,1,1,0,0,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0.34892149,0,1 @ F,0,0,1,0,0.18398697,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.34892149,0,1 @ F,0,0,1,0,0.18398697,0,1 @ F,0,1,1,0,0,0,1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect918"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,2.00309,0,1 @ F,0,0,1,0,2.00309,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,2.00309,0,1 @ F,0,0,1,0,2.00309,0,1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect912"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.0000002,0,1 @ F,0,0,1,0,0,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,7.0000002,0,1 @ F,0,0,1,0,0,0,1" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect894"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0.49832891,0,1 @ F,0,0,1,0,0.49832891,0,1 @ F,0,1,1,0,0.5316463,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49999976,0,1 @ F,0,0,1,0,0.50000024,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,0,1,0,0.99999982,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0295214,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5316463,0,1"
unit="px"
method="auto"
mode="F"
radius="0"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false"
nodesatellites_param="F,0,0,1,0,0.49832891,0,1 @ F,0,0,1,0,0.49832891,0,1 @ F,0,1,1,0,0.5316463,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49999976,0,1 @ F,0,0,1,0,0.50000024,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0000001,0,1 @ F,0,0,1,0,0.99999982,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0295214,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5316463,0,1" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#232629"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="22.390336"
inkscape:cx="13.4433"
inkscape:cy="12.572388"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-text-baseline="true"
showguides="false"
inkscape:guide-bbox="true"
inkscape:window-width="956"
inkscape:window-height="1036"
inkscape:window-x="960"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#232629">
<inkscape:grid
type="xygrid"
id="grid833"
empspacing="22"
dotted="true" />
<inkscape:grid
type="xygrid"
id="grid835"
empspacing="1"
spacingy="1.0583333"
spacingx="1.0583333"
originx="0.26458333"
originy="0.26458333" />
<sodipodi:guide
position="4.195,4.2333333"
orientation="0.96592583,0.25881905"
id="guide887"
inkscape:label=""
inkscape:locked="false"
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="0.26458333,1.9843751"
orientation="0,-1"
id="guide900"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path924"
style="opacity:1;fill:#fdbc4b;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:531.552;stroke-opacity:1;paint-order:markers fill stroke"
d="m -1.3252903,1.5032615 -0.4100651,0.6834418 a 0.12845631,0.12845631 60.481872 0 0 0.1101504,0.1945466 l 0.2495783,-10e-8 -0.1100188,0.5495576 a 0.08982544,0.08982544 21.142221 0 0 0.1651025,0.063848 L -0.91047797,2.3112131 A 0.12845634,0.12845634 60.481877 0 0 -1.0206284,2.1166665 H -1.2702067 L -1.1601879,1.567109 a 0.08982541,0.08982541 21.142225 0 0 -0.1651024,-0.063848 z"
sodipodi:nodetypes="ccccccc"
inkscape:original-d="m -1.0583333,1.0583332 -0.79375,1.3229167 0.4764566,-10e-8 L -1.5875,3.4395832 -0.79375003,2.1166665 H -1.2702067 Z"
inkscape:path-effect="#path-effect950" />
<g
id="g954"
transform="matrix(0.38576851,0,0,0.38576851,152.56385,-12.494408)">
<rect
y="-7.9375"
x="-357.1875"
height="79.375"
width="68.791664"
id="rect919"
style="fill:#cacaca;fill-opacity:1;stroke-width:14.6855;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
y="-5.2916665"
x="-354.54169"
height="10.583333"
width="10.583333"
id="rect851"
style="fill:#e74c3c;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#ed1515;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect853"
width="10.583333"
height="10.583333"
x="-343.95834"
y="-5.2916665" />
<rect
y="-5.2916665"
x="-333.375"
height="10.583333"
width="10.583333"
id="rect855"
style="fill:#f47750;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#fdbc4b;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect857"
width="10.583333"
height="10.583333"
x="-322.79169"
y="-5.2916665" />
<rect
y="-5.2916665"
x="-312.20834"
height="10.583333"
width="10.583333"
id="rect859"
style="fill:#c9ce3b;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#da4453;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect861"
width="10.583333"
height="10.583333"
x="-354.54169"
y="5.291667" />
<rect
y="5.291667"
x="-343.95834"
height="10.583333"
width="10.583333"
id="rect863"
style="fill:#c0392b;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#d35400;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect865"
width="10.583333"
height="10.583333"
x="-333.375"
y="5.291667" />
<rect
y="5.291667"
x="-322.79169"
height="10.583333"
width="10.583333"
id="rect867"
style="fill:#f67400;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#f39c1f;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect869"
width="10.583333"
height="10.583333"
x="-312.20834"
y="5.291667" />
<rect
style="fill:#1cdc9a;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect871"
width="10.583333"
height="10.583333"
x="-354.54169"
y="21.166666" />
<rect
y="21.166666"
x="-343.95834"
height="10.583333"
width="10.583333"
id="rect873"
style="fill:#2ecc71;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#1abc9c;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect875"
width="10.583333"
height="10.583333"
x="-333.375"
y="21.166666" />
<rect
y="21.166666"
x="-322.79169"
height="10.583333"
width="10.583333"
id="rect877"
style="fill:#3498db;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#3daee9;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect879"
width="10.583333"
height="10.583333"
x="-312.20834"
y="21.166666" />
<rect
y="31.75"
x="-354.54169"
height="10.583333"
width="10.583333"
id="rect881"
style="fill:#10d116;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#27ae60;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect883"
width="10.583333"
height="10.583333"
x="-343.95834"
y="31.75" />
<rect
y="31.75"
x="-333.375"
height="10.583333"
width="10.583333"
id="rect885"
style="fill:#16a085;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#2980b9;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect887"
width="10.583333"
height="10.583333"
x="-322.79169"
y="31.75" />
<rect
y="31.75"
x="-312.20834"
height="10.583333"
width="10.583333"
id="rect889"
style="fill:#1d99f3;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
y="21.166666"
x="-301.625"
height="10.583333"
width="10.583333"
id="rect891"
style="fill:#9b59b6;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#8e44ad;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect893"
width="10.583333"
height="10.583333"
x="-301.625"
y="31.75" />
<rect
y="47.625"
x="-354.54166"
height="10.583333"
width="10.583333"
id="rect895"
style="fill:#34495e;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#31363b;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect897"
width="10.583333"
height="10.583333"
x="-343.95831"
y="47.625" />
<rect
y="47.625"
x="-333.37497"
height="10.583333"
width="10.583333"
id="rect899"
style="fill:#7f8c8d;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#bdc3c7;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect901"
width="10.583333"
height="10.583333"
x="-322.79166"
y="47.625" />
<rect
y="47.625"
x="-312.20831"
height="10.583333"
width="10.583333"
id="rect903"
style="fill:#fcfcfc;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#2c3e50;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect905"
width="10.583333"
height="10.583333"
x="-354.54166"
y="58.208336" />
<rect
y="58.208336"
x="-343.95831"
height="10.583333"
width="10.583333"
id="rect907"
style="fill:#232629;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#4d4d4d;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect909"
width="10.583333"
height="10.583333"
x="-333.37497"
y="58.208336" />
<rect
y="58.208336"
x="-322.79166"
height="10.583333"
width="10.583333"
id="rect911"
style="fill:#95a5a6;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552" />
<rect
style="fill:#eff0f1;fill-opacity:1;stroke-width:14.955;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:531.552"
id="rect913"
width="10.583333"
height="10.583333"
x="-312.20831"
y="58.208336" />
</g>
<path
transform="scale(0.26458333)"
id="path887"
d="M 13.994141 4.5 A 0.49999949 0.49999949 0 0 0 13.552734 4.7773438 A 0.49999949 0.49999949 0 0 0 13.777344 5.4472656 L 14.908203 6.0136719 C 15.328243 6.2241517 15.636226 6.6066586 15.75 7.0625 L 17.390625 13.623047 C 15.381402 13.909129 13.837817 15.475476 13.601562 17.5 L 8.3984375 17.5 C 8.1378934 15.267332 6.3043435 13.50201 4.0039062 13.5 C 3.982495 13.499838 3.9608645 13.499856 3.9394531 13.5 L 2 13.5 A 0.49999949 0.49999949 0 0 0 1.5 14 A 0.49999949 0.49999949 0 0 0 2 14.5 L 3.9394531 14.5 A 0.50004949 0.50004949 0 0 0 3.9433594 14.5 C 3.9610145 14.499866 3.9784387 14.499866 3.9960938 14.5 A 0.50004949 0.50004949 0 0 0 4 14.5 C 5.9389193 14.5 7.5000003 16.061081 7.5 18 A 0.50004949 0.50004949 0 0 0 8 18.5 L 14 18.5 A 0.50004949 0.50004949 0 0 0 14.5 18 C 14.5 16.061081 16.061081 14.5 18 14.5 A 0.50004949 0.50004949 0 0 0 18.484375 13.878906 L 16.71875 6.8203125 C 16.534472 6.0819921 16.037756 5.460052 15.357422 5.1191406 A 0.50004949 0.50004949 0 0 0 15.355469 5.1191406 L 14.222656 4.5527344 A 0.49999949 0.49999949 0 0 0 13.994141 4.5 z "
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00157481;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:531.552;stroke-opacity:1;paint-order:markers fill stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1" />
<path
id="path970"
d="M 4.7617188 4.1367188 C 4.4265639 4.1367188 4.1523438 4.4109389 4.1523438 4.7460938 C 4.1523438 5.0812486 4.4265639 5.3554687 4.7617188 5.3554688 C 5.0968736 5.3554687 5.3710937 5.0812486 5.3710938 4.7460938 C 5.3710937 4.4109389 5.0968736 4.1367188 4.7617188 4.1367188 z M 4.7617188 4.3378906 C 4.9879388 4.3378906 5.1679687 4.5198737 5.1679688 4.7460938 C 5.1679687 4.9723138 4.9879388 5.1542969 4.7617188 5.1542969 C 4.5354987 5.1542969 4.3535156 4.9723138 4.3535156 4.7460938 C 4.3535156 4.5198737 4.5354987 4.3378906 4.7617188 4.3378906 z "
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.201567;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:0.997" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.997;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.201567;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 1.0221536,4.1367188 c -0.33515491,0 -0.60937501,0.2742201 -0.60937501,0.609375 0,0.3351548 0.2742201,0.6093749 0.60937501,0.609375 0.3351548,-10e-8 0.6093749,-0.2742202 0.609375,-0.609375 -1e-7,-0.3351549 -0.2742202,-0.609375 -0.609375,-0.609375 z m 0,0.2011718 c 0.22622,0 0.4062499,0.1819831 0.40625,0.4082032 -1e-7,0.2262201 -0.18003,0.4082031 -0.40625,0.4082031 -0.22622011,0 -0.40820321,-0.181983 -0.40820321,-0.4082031 0,-0.2262201 0.1819831,-0.4082032 0.40820321,-0.4082032 z"
id="path970-7" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,5 @@
v4.8.1: OpenTracks
Bugfix:
- Location permission was not granted due to ACCESS_LOCATION_BACKGROUND (removed)
@@ -0,0 +1,5 @@
v4.8.2: OpenTracks
Changes:
- After recording export: show error message if unsuccessful
@@ -0,0 +1,8 @@
v4.8.3: OpenTracks
Bugfix:
- Android 14: prevent crash due to malformed UUID (example for filename template)
Developer:
- Marker list is now a RecyclerView
@@ -0,0 +1,7 @@
v4.8.4: OpenTracks
Changes:
- Show max power for recorded tracks
Developer:
- TrackList now uses RecyclerView
@@ -0,0 +1,23 @@
v4.9.0: OpenTracks
NOTE: BREAKING CHANGE
This release changes how OpenTracks determines moving and not moving.
Until now, the speed (GPS or sensor) was used by a applying a threshold (e.g., less than 0.3 m/s).
However, this may be very noise.
From now on, a timeout duration is used (i.e., no significant change of position in the last X seconds).
This duration is user configurable.
The events of becoming idle are stored in the database incl. a timestamp, so statistics can be re-computed.
A downgrade from this version is not implemented.
Please backup all before upgrading.
Changes:
- Idle detection uses "no movement" duration rather than speed too low
- Added scooter activity
Bugfix:
- While recording, TrackList showed all tracks as recording
Developer:
- Re-enabled Github action executing tests
@@ -0,0 +1,4 @@
v4.3.4: OpenTracks
Bugfix:
- Android 13: regional locales could not be selected (introduced in v4.3.0)
@@ -0,0 +1,5 @@
v4.3.5: OpenTracks
Bugfix:
- Sensor parsing had a overflow one off bug (#1559)
- Insert marker: don't crash if no camera app is installed (#1561)
@@ -0,0 +1,8 @@
v4.7.1: OpenTracks
Arranxos:
- Android 14: fallo ao inicio da gravación (por falta de permisos)
- Android 14: fallo ao eliminar unha ruta (sen razóns para ForegroundService)
Desenvolvemento:
- agora AggregatedStats é unha RecyclerView
@@ -0,0 +1,10 @@
v4.8.0: OpenTracks
Cambios:
- Permiso: ACCESS_BACKGROUND_LOCATION é requerido debido á API
- Preparación para a detección da pausa en función do tempoPreparation for time-based idle detection
- Retirada da distinción por orixe nos datos gardados (sensores GPS vs. BLE)
Desenvolvemento:
- retirados algúns atallos no código que entraban en conflito (TrackRecordingService e GpsStatusManager)
- TrackRecordingServiceConnection xestiona o apagado do servizo
@@ -0,0 +1,4 @@
v4.8.1: OpenTracks
Arranxo:
- Non se concedía o permiso de localización debido a ACCESS_LOCATION_BACKGROUND (eliminado)
@@ -0,0 +1,4 @@
v4.8.2: OpenTracks
Cambios:
- Apos exportar as gravacións: mostra mensaxe de aviso se falla
@@ -0,0 +1,8 @@
v4.8.3: OpenTracks
Arranxos:
- Android 14: evitar o peche da app debido a un UUID mal formado (ex. no modelo do nome de ficheiro)
Desenvolvemento:
- A lista Marker agora usa RecyclerView
@@ -0,0 +1,7 @@
v4.8.4: OpenTracks
Cambios:
- Mostra a potencia máxima nas rutas gravadas
Desenvolvemento:
- agora TrackList usa RecyclerView
@@ -0,0 +1,10 @@
v4.8.0: OpenTracks
Veranderingen:
- Toestemming: ACCESS_BACKGROUND_LOCATION is vereist vanwege API
- Voorbereiding voor tijdgebaseerde stationaire detectie
- Onderscheid bron (GPS vs BLE sensoren) vervallen voor opgeslagen gegevens
Ontwikkelaar:
- verwijderde raceconditie-werkplekken (TrackRecordingService en GpsStatusManager)
- TrackRecordingServiceConnection zorgt voor service-uitschakeling
@@ -0,0 +1,4 @@
v4.8.1: OpenTracks
Bugfix :
- Toestemming tot locatie is niet verleend vanwege ACCESS_LOCATION_BACKGROUND (verwijderd)
@@ -0,0 +1,4 @@
v4.8.2: OpenTracks
Veranderingen:
- Na export van opname: toon foutmelding indien niet succesvol
@@ -0,0 +1,7 @@
v4.8.3: OpenTracks
Bugfix :
- Android 14: crash voorkomen t.g.v. misvormde UUID (voorbeeld voor bestandsnaam sjabloon)
Ontwikkelaar:
- Markeringslijst is nu een RecyclerView
@@ -0,0 +1,7 @@
v4.8.4: OpenTracks
Veranderingen:
- Toon maximale vermogen voor opgenomen tracks
Ontwikkelaar:
- TrackList gebruikt nu RecyclerView
Binary file not shown.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+4 -1
View File
@@ -130,10 +130,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -8,6 +8,8 @@ import static androidx.test.espresso.action.ViewActions.longClick;
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.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
@@ -49,7 +51,7 @@ public class EspressoDeleteTrackTest {
onView(withId(R.id.finish_button)).perform(click());
// select track
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
// open menu and delete selected track
//TODO openActionBarOverflowOrOptionsMenu(); doesn't work
@@ -8,6 +8,8 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
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.withParentIndex;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
@@ -95,6 +97,6 @@ public class EspressoUITest {
@Test
public void selectAndDeleteTrack() {
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
}
}
@@ -9,8 +9,8 @@ public class TestUtil {
public static GrantPermissionRule createGrantPermissionRule() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.POST_NOTIFICATIONS);
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.POST_NOTIFICATIONS);
}
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT);
return GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.BLUETOOTH_CONNECT);
}
}
@@ -37,7 +37,7 @@ public class TestDataUtil {
public static Track createTrack(Track.Id trackId) {
Track track = new Track();
track.setId(trackId);
track.setName("Test: " + trackId.getId());
track.setName("Test: " + trackId.id());
return track;
}
@@ -119,7 +119,7 @@ public class CustomContentProviderUtilsTest {
assertNotNull(trackPoint);
locations.add(trackPoint);
// Make sure the IDs are returned in the right order.
assertEquals(lastPointId.getId() - numPoints + locations.size(), trackPoint.getId().getId());
assertEquals(lastPointId.id() - numPoints + locations.size(), trackPoint.getId().id());
}
assertEquals(numPoints, locations.size());
}
@@ -128,7 +128,7 @@ public class CustomContentProviderUtilsTest {
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id.getId());
track.setName("Test: " + id.id());
contentProviderUtils.insertTrack(track);
track = contentProviderUtils.getTrack(id);
assertNotNull(track);
@@ -155,7 +155,7 @@ public class CustomContentProviderUtilsTest {
}
}
assertTrue(numPoints == 0 || lastPointId.getId() > 0);
assertTrue(numPoints == 0 || lastPointId.id() > 0);
assertEquals(numPoints, counter);
return lastPointId;
@@ -172,7 +172,7 @@ public class CustomContentProviderUtilsTest {
// Id
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getLong(columnIndex)).thenReturn(trackId.getId());
when(cursorMock.getLong(columnIndex)).thenReturn(trackId.id());
//Uuid
columnIndex++;
@@ -182,7 +182,7 @@ public class CustomContentProviderUtilsTest {
// Name
columnIndex++;
String name = NAME_PREFIX + trackId.getId();
String name = NAME_PREFIX + trackId.id();
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getString(columnIndex)).thenReturn(name);
@@ -460,7 +460,7 @@ public class CustomContentProviderUtilsTest {
Marker marker = contentProviderUtils.createMarker(cursorMock);
assertEquals(id, marker.getId().id());
assertEquals(name, marker.getName());
assertEquals(trackId, marker.getTrackId().getId());
assertEquals(trackId, marker.getTrackId().id());
}
/**
@@ -640,7 +640,7 @@ public class CustomContentProviderUtilsTest {
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.id());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
@@ -675,7 +675,7 @@ public class CustomContentProviderUtilsTest {
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.id());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
@@ -712,7 +712,7 @@ public class CustomContentProviderUtilsTest {
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
contentProviderUtils.insertMarker(otherMarker);
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.id());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(2, dir.list().length);
@@ -889,7 +889,7 @@ public class CustomContentProviderUtilsTest {
for (int i = 0; i < trackpointIds.size(); i++) {
assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
assertEquals(startTrackPointId.getId() + i, trackPoint.getId().getId());
assertEquals(startTrackPointId.id() + i, trackPoint.getId().id());
checkLocation(i, trackPoint.getLocation());
}
@@ -1,33 +1,16 @@
package de.dennisguse.opentracks.data.model;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackPointTest {
@Test
public void isRecent_true() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.now());
assertTrue(tp.isRecent());
}
@Test
public void isRecent_false() {
TrackPoint tp = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.now().minus(2, ChronoUnit.MINUTES));
assertFalse(tp.isRecent());
}
@Test
public void distanceToPrevious() {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0))
@@ -5,8 +5,8 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Track;
@@ -18,7 +18,7 @@ public class TrackFilenameGenerator2Test {
@Parameterized.Parameters
public static Collection<String> data() {
return Arrays.asList(
return List.of(
"{name}_{starime}",
"{name",
"name}");
@@ -35,7 +35,7 @@ public class TrackFilenameGenerator2Test {
// given
Track track = new Track();
track.setName("Best Track");
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
track.setUuid(UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"));
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
// when
@@ -1,59 +0,0 @@
package de.dennisguse.opentracks.io.file.exporter;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.time.Instant;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@RunWith(JUnit4.class)
public class KmlTrackExporterTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Sensor data by type should only be created if present in at least on TrackPoint.
*/
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = """
<when>1970-01-01T00:00:00Z</when>
<coord/>
<when>1970-01-01T01:00:00+01:00</when>
<coord/>
<ExtendedData>
<SchemaData schemaUrl="#schema">
</SchemaData>
</ExtendedData>
</Track>
""";
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
KMLTrackExporter kmlTrackWriter = (KMLTrackExporter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, null);
kmlTrackWriter.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(ZoneOffset.UTC, trackPoint);
kmlTrackWriter.writeTrackPoint(ZoneOffset.ofTotalSeconds(3600), trackPoint);
// when
kmlTrackWriter.writeCloseSegment();
kmlTrackWriter.close();
// then
assertEquals(expected, outputStream.toString());
}
}
@@ -35,6 +35,7 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
@@ -168,9 +169,12 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:03:30Z");
service.getTrackRecordingManager().onIdle();
trackPointCreator.setClock("2020-02-02T02:03:24Z");
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
@@ -182,8 +186,103 @@ public class ExportImportTest {
track = contentProviderUtils.getTrack(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(12, trackPoints.size());
assertEquals(2, markers.size());
}
@LargeTest
@Test
public void track() throws TimeoutException {
setUp();
Track track = contentProviderUtils.getTrack(trackId);
TrackStatistics trackStatistics = track.getTrackStatistics();
assertEquals(ZoneOffset.of("+01:00"), track.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), trackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:04:00Z"), trackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(56), trackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(26), trackStatistics.getMovingTime()); //TODO Likely too low
// Distance
assertEquals(222125.53125, trackStatistics.getTotalDistance().toM(), 0.01); //TODO Too low
// Speed
assertEquals(8543.29, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(3966.52, trackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8543.28, trackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, trackStatistics.getMinAltitude(), 0.01);
assertEquals(10, trackStatistics.getMaxAltitude(), 0.01);
assertEquals(2, trackStatistics.getTotalAltitudeGain(), 0.01);
assertEquals(2, trackStatistics.getTotalAltitudeLoss(), 0.01);
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:02:02Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:03Z"))
.setLatitude(3)
.setLongitude(14)
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:04Z"))
.setSensorDistance(Distance.of(10))
.setSpeed(Speed.of(15))
.setHeartRate(HeartRate.of(66))
.setCadence(3)
.setPower(50)
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:15Z"))
.setHeartRate(HeartRate.of(68))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:17Z"))
.setLatitude(3)
.setLongitude(14.001)
.setAltitude(10)
.setSensorDistance(Distance.of(2))
.setSpeed(Speed.of(5))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10))
.setHeartRate(HeartRate.of(69))
.setCadence(3)
.setPower(50),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:21Z"))
.setLatitude(3)
.setLongitude(14.002)
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:22Z"))
.setLatitude(3)
.setLongitude(16)
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z")),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:50Z"))
.setLatitude(3)
.setLongitude(16.001)
.setAltitude(10)
.setSpeed(Speed.of(15))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
}
//TODO Does not test marker images
@@ -225,43 +324,7 @@ public class ExportImportTest {
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:03:24Z"), importedTrackStatistics.getStopTime());
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(20), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(222238.70, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(55559.67, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(11111.93, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(55559.67, 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(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);
assertEquals(track.getTrackStatistics(), importedTrackStatistics);
// 4. markers
assertMarkers();
@@ -347,7 +410,7 @@ public class ExportImportTest {
.setSpeed(Speed.of(5))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(14))
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f)
@@ -368,7 +431,7 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:23Z"))
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:50Z"))
.setLatitude(3)
.setLongitude(16.001)
.setAltitude(10)
@@ -379,24 +442,23 @@ public class ExportImportTest {
), 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:03:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:03:50Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(80), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(80), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(107), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(222349.85, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(222347.85, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(2779.37, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(2779.37, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(2779.37, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(2078.01, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(2078.01, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(2078.01, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -487,14 +549,14 @@ public class ExportImportTest {
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence);
if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
@@ -141,8 +141,8 @@ public class GPXTrackImporterTest {
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(1.44, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(53), trackStatistics.getMovingTime());
assertEquals(0.75, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
@@ -189,7 +189,7 @@ public class GPXTrackImporterTest {
// 3. trackstatistics
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
assertEquals(4.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(5.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
// 4. trackpoints
@@ -146,6 +146,7 @@ public class TrackRecordingServiceMarkerTest {
assertEquals(0.0, wpt.getLength().toM(), 0.01);
assertNotNull(wpt.getLocation());
trackPointCreator.setClock("2020-02-02T02:02:04Z");
service.endCurrentTrack();
}
}
@@ -88,7 +88,7 @@ public class TrackRecordingServiceRecordingTest {
contentProviderUtils = new ContentProviderUtils(context);
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);
PreferencesUtils.setString(R.string.idle_duration_key, R.string.idle_duration_default);
service = startService();
}
@@ -122,7 +122,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 1, 0, 0, 0f, 0f)
assertEquals(new TrackStatistics(startTime, stopTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
@@ -150,7 +150,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 0, 0, 0f, 0f)
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
@@ -166,7 +166,7 @@ public class TrackRecordingServiceRecordingTest {
service.resumeTrack(trackId);
// then
assertEquals(new TrackStatistics(startTime, resumeTime, 0, 1, 0, 0, 0f, 0f)
assertEquals(new TrackStatistics(startTime, resumeTime, 0, 1, 1, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
@@ -299,7 +299,7 @@ public class TrackRecordingServiceRecordingTest {
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f)
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
@@ -307,7 +307,7 @@ public class TrackRecordingServiceRecordingTest {
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0001, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps2, 11.113178253173828f, 4, 3, 15, 0f, 0f)
assertEquals(new TrackStatistics(startTime, gps2, 11.113178253173828f, 4, 4, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
@@ -315,7 +315,7 @@ public class TrackRecordingServiceRecordingTest {
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0002, 35.0, 1, 15);
// then
assertEquals(new TrackStatistics(startTime, gps3, 22.226356506347656, 6, 5, 15, 0f, 0f)
assertEquals(new TrackStatistics(startTime, gps3, 22.226356506347656, 6, 6, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
@@ -325,7 +325,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 22.226356506347656, 10, 5, 15, 0f, 0f)
assertEquals(new TrackStatistics(startTime, stopTime, 22.226356506347656, 10, 10, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
@@ -357,71 +357,6 @@ public class TrackRecordingServiceRecordingTest {
), 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.getSensorManager().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() {
@@ -437,7 +372,7 @@ public class TrackRecordingServiceRecordingTest {
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
// then
TrackStatistics gps1Statistics = new TrackStatistics(startTime, gps1, 0, 1, 0, 0, 0f, 0f);
TrackStatistics gps1Statistics = new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 0f, 0f);
assertEquals(gps1Statistics, contentProviderUtils.getTrack(trackId).getTrackStatistics());
// when
@@ -461,7 +396,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 2.222635507583618, 10, 5, 15, 0f, 0f)
assertEquals(new TrackStatistics(startTime, stopTime, 2.222635507583618, 10, 10, 15, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
new TrackPointAssert().assertEquals(List.of(
@@ -486,158 +421,6 @@ public class TrackRecordingServiceRecordingTest {
), 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.getSensorManager().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.getSensorManager().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_recordingDistance_movement_non_idle() {
@@ -721,7 +504,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 0, 0, 0f, 0f)
assertEquals(new TrackStatistics(startTime, stopTime, 0, 10, 10, 0, 0f, 0f)
, contentProviderUtils.getTrack(trackId).getTrackStatistics());
@@ -749,14 +532,14 @@ public class TrackRecordingServiceRecordingTest {
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());
assertEquals(new TrackStatistics(startTime, gps1, 0, 1, 1, 15, 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, 11113.275390625, 4, 3, 3704.4251302083335f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
assertEquals(new TrackStatistics(startTime, gps2, 11113.275390625, 4, 4, 2778.31884765625f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
// when
@@ -765,7 +548,7 @@ public class TrackRecordingServiceRecordingTest {
service.endCurrentTrack();
// then
assertEquals(new TrackStatistics(startTime, stopTime, 11113.275390625, 10, 3, 3704.4251302083335f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
assertEquals(new TrackStatistics(startTime, stopTime, 11113.275390625, 10, 10, 1111.3275390625f, 0f, 0f).toString(), contentProviderUtils.getTrack(trackId).getTrackStatistics().toString());
// then
@@ -63,7 +63,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
@@ -79,7 +79,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
@@ -95,7 +95,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
@@ -111,7 +111,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
@@ -133,7 +133,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
@@ -149,7 +149,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, false, null, null).toString();
// then
assertEquals("Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
@@ -171,7 +171,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
@@ -187,7 +187,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
@@ -201,7 +201,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
@@ -215,7 +215,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_METER, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_METER, true, null, null).toString();
// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
@@ -229,7 +229,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setMovingTime(Duration.ofHours(1));
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
@@ -251,7 +251,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
@@ -267,7 +267,7 @@ public class VoiceAnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, false, null, null).toString();
// then
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
@@ -289,7 +289,7 @@ public class VoiceAnnouncementUtilsTest {
}
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString();
// then
assertEquals("Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
@@ -312,10 +312,10 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals("Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
@@ -342,10 +342,10 @@ public class VoiceAnnouncementUtilsTest {
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
String announcement = VoiceAnnouncementUtils.createStatistics(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
@@ -83,35 +83,17 @@ public class TrackStatisticsUpdaterTest {
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(142.26, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(12), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(10), statistics.getMovingTime());
assertEquals(Duration.ofSeconds(12), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(11.85, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(11.85, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(11.85, statistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(106.64f, statistics.getAverageHeartRate().getBPM(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
// when
subject.addTrackPoint(tp1);
subject.addTrackPoint(tp2);
subject.addTrackPoint(tp3);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(106.834f, statistics.getAverageHeartRate().getBPM(), 0.01);
}
@Test
@@ -165,35 +147,6 @@ public class TrackStatisticsUpdaterTest {
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving_and_sensor_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
// when
subject.addTrackPoint(tp1);
subject.addTrackPoint(tp2);
subject.addTrackPoint(tp3);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4);
subject.addTrackPoint(tp5);
// then
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
@@ -225,42 +178,6 @@ public class TrackStatisticsUpdaterTest {
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Test
public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
// Ignore as we set max speed if two consecutive trackpoints were considered moving
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(1f)));
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
// Update max speed
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(1f)));
assertEquals(Speed.of(1f), subject.getTrackStatistics().getMaxSpeed());
// Update max speed
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(12))
.setSpeed(Speed.of(50f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
// Ignore; we were getting slower
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(13))
.setSpeed(Speed.of(5f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
// Ignore acceleration above 2g
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(14))
.setSpeed(Speed.of(500f)));
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
}
@Test
public void addTrackPoint_maxSpeed_multiple_segments() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
@@ -290,6 +207,64 @@ public class TrackStatisticsUpdaterTest {
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
}
@Test
public void addTrackPoint_idle_withoutDistance() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
// when
subject.addTrackPoints(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSpeed(Speed.of(2f)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSpeed(Speed.of(2f)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
new TrackPoint(0, 1, Altitude.WGS84.of(0), Instant.ofEpochSecond(50)),
new TrackPoint(0, 2, Altitude.WGS84.of(0), Instant.ofEpochSecond(55)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60))
));
// then
assertEquals(Duration.ofSeconds(40), subject.getTrackStatistics().getMovingTime());
}
@Test
public void addTrackPoint_idle_withDistance() {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
// when
subject.addTrackPoints(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
.setSensorDistance(Distance.of(10)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.IDLE, Instant.ofEpochSecond(30))
.setSensorDistance(Distance.ofKilometer(1)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(40))
.setHeartRate(50),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochSecond(45))
.setHeartRate(50),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(50))
.setSensorDistance(Distance.of(10)),
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(55))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(60))
));
// then
assertEquals(Duration.ofSeconds(45), subject.getTrackStatistics().getMovingTime());
assertEquals(Distance.of(1040), subject.getTrackStatistics().getTotalDistance());
}
@Test
public void copy_constructor() {
// given
@@ -314,7 +289,6 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp5);
copy.addTrackPoint(tp5);
// then
assertEquals(55.287, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
assertEquals(55.287, copy.getTrackStatistics().getTotalDistance().toM(), 0.01);
@@ -75,6 +75,6 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_number() {
PreferencesUtils.setString(R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
assertEquals("Track " + TRACK_ID.id(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
}
+3 -3
View File
@@ -3,11 +3,11 @@
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,14,10,10,,54,1,1,,,,
"2020-02-02T03:02:04+01:00","TRACKPOINT",,,,,,54,1,1,10,66,3,50
"2020-02-02T03:02:15+01:00","TRACKPOINT",,,,,,,,,,68,3,50
"2020-02-02T03:02:16+01:00","TRACKPOINT",,,,,,18,,,2,69,3,50
"2020-02-02T03:02:17+01:00","TRACKPOINT",3,14.001,10,10,,18,0,0,2,69,3,50
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:21+01:00","TRACKPOINT",3,14.002,10,10,,54,0,0,,,,
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3,16,10,10,,54,0,0,,,,
"2020-02-02T03:03:23+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,,
"2020-02-02T03:03:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,,,,,,
"2020-02-02T03:03:50+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,,
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
1 #time trackpoint_type latitude longitude altitude accuracy_horizontal accuracy_vertical speed altitude_gain altitude_loss sensor_distance heartrate cadence power
3 2020-02-02T03:02:03+01:00 TRACKPOINT 3 14 10 10 54 1 1
4 2020-02-02T03:02:04+01:00 TRACKPOINT 54 1 1 10 66 3 50
5 2020-02-02T03:02:15+01:00 TRACKPOINT 68 3 50
2020-02-02T03:02:16+01:00 TRACKPOINT 18 2 69 3 50
6 2020-02-02T03:02:17+01:00 TRACKPOINT 3 14.001 10 10 18 0 0 2 69 3 50
7 2020-02-02T03:02:18+01:00 SEGMENT_END_MANUAL
8 2020-02-02T03:03:20+01:00 SEGMENT_START_MANUAL
9 2020-02-02T03:03:21+01:00 TRACKPOINT 3 14.002 10 10 54 0 0
10 2020-02-02T03:03:22+01:00 SEGMENT_START_AUTOMATIC 3 16 10 10 54 0 0
11 2020-02-02T03:03:23+01:00 2020-02-02T03:03:30+01:00 TRACKPOINT IDLE 3 16.001 10 10 54 0 0
12 2020-02-02T03:03:24+01:00 2020-02-02T03:03:50+01:00 SEGMENT_END_MANUAL TRACKPOINT 3 16.001 10 10 54 0 0
13 2020-02-02T03:04:00+01:00 SEGMENT_END_MANUAL
-2
View File
@@ -31,8 +31,6 @@ xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:Ascent>null</gpxtrkx:Ascent>
<gpxtrkx:Descent>null</gpxtrkx:Descent>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
+3 -2
View File
@@ -46,7 +46,7 @@ limitations under the License.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />-->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -333,7 +333,8 @@ limitations under the License.
<activity
android:name=".TrackListActivity"
android:exported="true">
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
@@ -21,7 +21,7 @@ import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncement;
import de.dennisguse.opentracks.services.announcement.TTSManager;
/**
* @author Jimmy Shih
@@ -33,7 +33,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
// Set volume control stream for text to speech
setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM);
setVolumeControlStream(TTSManager.AUDIO_STREAM);
setContentView(getRootView());
}
@@ -45,8 +45,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
@Override
public void onConfirmDeleteDone(Track.Id... trackIds) {
ArrayList<Track.Id> trackIdList = new ArrayList<>(Arrays.asList(trackIds))
.stream().filter(trackId -> !trackId.equals(getRecordingTrackId())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<Track.Id> trackIdList = Arrays.stream(trackIds)
.filter(trackId -> !trackId.equals(getRecordingTrackId())).collect(Collectors.toCollection(ArrayList::new));
onDeleteConfirmed();
@@ -66,7 +66,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId.getId());
Log.e(TAG, "No track for " + trackId.id());
finish();
return;
}
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks;
import android.app.ActivityOptions;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
@@ -26,37 +25,26 @@ import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.button.MaterialButton;
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.Objects;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
@@ -66,15 +54,14 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.TrackListAdapter;
import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
import de.dennisguse.opentracks.util.StringUtils;
/**
* An activity displaying a list of tracks.
@@ -87,12 +74,10 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// The following are set in onCreate
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ResourceCursorAdapter resourceCursorAdapter;
private TrackListAdapter adapter;
private TrackListBinding viewBinding;
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
// Preferences
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
@@ -128,11 +113,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
unitSystem = PreferencesUtils.getUnitSystem();
if (adapter != null) {
adapter.updateUnitSystem(unitSystem);
}
}
if (key != null) {
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
loadData();
});
}
};
@@ -140,6 +128,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Menu items
private MenuItem searchMenuItem;
private String searchQuery;
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingStatusObservable()
.observe(TrackListActivity.this, this::onRecordingStatusChanged);
@@ -180,63 +170,10 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
});
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackIdId) -> {
Track.Id trackId = new Track.Id(trackIdId);
if (recordingStatus.isRecording() && trackId.equals(recordingStatus.getTrackId())) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
} else {
// Not recording -> open detail activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
this,
new Pair<>(view.findViewById(R.id.list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON));
startActivity(newIntent, activityOptions.toBundle());
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
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 activityTypeIndex = cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
boolean isRecording = trackId.equals(recordingStatus.getTrackId());
String icon = cursor.getString(iconIndex);
int iconId = ActivityType.findBy(icon)
.getIconDrawableId();
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(TrackListActivity.this).formatDistance(Distance.of(cursor.getDouble(totalDistanceIndex)));
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
int startTimeOffset = cursor.getInt(startTimeOffsetIndex);
String activityType = icon != null && !icon.equals("") ? null : cursor.getString(activityTypeIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording,
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)),
activityType, description, false);
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
adapter = new TrackListAdapter(this, viewBinding.trackList, recordingStatus, unitSystem);
viewBinding.trackList.setLayoutManager(layoutManager);
viewBinding.trackList.setAdapter(adapter);
viewBinding.trackListFabAction.setOnClickListener((view) -> {
if (recordingStatus.isRecording()) {
@@ -271,12 +208,11 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
});
setSupportActionBar(viewBinding.trackListToolbar);
loadData(getIntent());
adapter.setActionModeCallback(contextualActionModeCallback);
}
private void requestRequiredPermissions() {
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_LONG).show());
}
@Override
@@ -293,7 +229,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
loadData();
// Float button
setFloatButton();
@@ -312,6 +248,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
super.onDestroy();
viewBinding = null;
trackRecordingServiceConnection = null;
adapter = null;
}
@Override
@@ -333,10 +270,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false);
return super.onPrepareOptionsMenu(menu);
}
@@ -375,11 +308,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
return super.onKeyUp(keyCode, event);
}
@Override
public void overridePendingTransition(int enterAnim, int exitAnim) {
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
}
@Override
public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
@@ -387,8 +315,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
if (searchQuery != null) {
searchQuery = null;
loadData();
return;
}
@@ -396,19 +325,30 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
@Override
public void onNewIntent(Intent intent) {
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
} else {
searchQuery = null;
}
}
loaderCallbacks.setSearch(searchQuery);
private void loadData() {
viewBinding.trackListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.app_name)));
Cursor tracks = new ContentProviderUtils(this).searchTracks(searchQuery);
adapter.swapData(tracks);
if (tracks.getCount() == 0) {
viewBinding.trackListEmptyView.setVisibility(View.VISIBLE);
viewBinding.trackList.setVisibility(View.GONE);
} else {
viewBinding.trackListEmptyView.setVisibility(View.GONE);
viewBinding.trackList.setVisibility(View.VISIBLE);
}
}
@Override
@@ -424,7 +364,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
@Nullable
@Override
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
return recordingStatus.trackId();
}
/**
@@ -490,64 +430,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
}
adapter.setAllSelected(true);
return false;
}
return false;
}
private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
viewBinding.trackListToolbar.setTitle(searchQuery == null ? getString(R.string.app_name) : searchQuery);
}
public void restart() {
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME, TracksColumns.STARTTIME_OFFSET,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
if (searchQuery == null) {
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder);
} else {
final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.ACTIVITY_TYPE_LOCALIZED + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
public void onGpsStatusChanged(GpsStatusValue newStatus) {
gpsStatusValue = newStatus;
updateGpsMenuItem(true, recordingStatus.isRecording());
@@ -561,5 +450,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private void onRecordingStatusChanged(RecordingStatus status) {
recordingStatus = status;
setFloatButton();
adapter.updateRecordingStatus(recordingStatus);
}
}
@@ -226,7 +226,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
@Nullable
@Override
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
return recordingStatus.trackId();
}
@Override
@@ -189,13 +189,13 @@ public class ContentProviderUtils {
}
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(trackId -> Long.toString(trackId.id())).toArray(String[]::new));
}
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
//TODO Only use for tests; also move to tests.
@@ -228,8 +228,33 @@ public class ContentProviderUtils {
return tracks;
}
public Cursor searchTracks(String searchQuery) {
// Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested.
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.UUID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.ACTIVITY_TYPE_LOCALIZED, TracksColumns.STARTTIME,
TracksColumns.STARTTIME_OFFSET, TracksColumns.STOPTIME, TracksColumns.MARKER_COUNT,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.MOVINGTIME,
TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED, TracksColumns.MAXSPEED,
TracksColumns.MIN_ALTITUDE, TracksColumns.MAX_ALTITUDE, TracksColumns.ALTITUDE_GAIN,
TracksColumns.ALTITUDE_LOSS, TracksColumns.ICON
};
String selection = null;
String[] selectionArgs = null;
final String sortOrder = TracksColumns.STARTTIME + " DESC";
if (searchQuery != null) {
selection = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.ACTIVITY_TYPE_LOCALIZED + " LIKE ?";
selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
}
return contentResolver.query(TracksColumns.CONTENT_URI, PROJECTION, selection, selectionArgs, sortOrder);
}
public Track getTrack(@NonNull Track.Id trackId) {
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())}, null)) {
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
}
@@ -278,7 +303,7 @@ public class ContentProviderUtils {
* @param track the track
*/
public void updateTrack(Track track) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.getId().getId())});
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.getId().id())});
}
private ContentValues createContentValues(Track track) {
@@ -286,7 +311,7 @@ public class ContentProviderUtils {
TrackStatistics trackStatistics = track.getTrackStatistics();
if (track.getId() != null) {
values.put(TracksColumns._ID, track.getId().getId());
values.put(TracksColumns._ID, track.getId().id());
}
values.put(TracksColumns.UUID, UUIDUtils.toBytes(track.getUuid()));
values.put(TracksColumns.NAME, track.getName());
@@ -315,7 +340,7 @@ public class ContentProviderUtils {
}
public void updateTrackStatistics(@NonNull Track.Id trackId, @NonNull TrackStatistics trackStatistics) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(trackStatistics), TracksColumns._ID + "=?", new String[]{Long.toString(trackId.id())});
}
private ContentValues createContentValues(TrackStatistics trackStatistics) {
@@ -413,7 +438,7 @@ public class ContentProviderUtils {
public Integer getNextMarkerNumber(@NonNull Track.Id trackId) {
String[] projection = {MarkerColumns._ID};
String selection = MarkerColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
String[] selectionArgs = new String[]{Long.toString(trackId.id())};
try (Cursor cursor = getMarkerCursor(projection, selection, selectionArgs, MarkerColumns._ID, -1)) {
if (cursor != null) {
return cursor.getCount();
@@ -443,10 +468,10 @@ public class ContentProviderUtils {
String[] selectionArgs;
if (minMarkerId != null) {
selection = MarkerColumns.TRACKID + "=? AND " + MarkerColumns._ID + ">=?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(minMarkerId.id())};
selectionArgs = new String[]{Long.toString(trackId.id()), Long.toString(minMarkerId.id())};
} else {
selection = MarkerColumns.TRACKID + "=?";
selectionArgs = new String[]{Long.toString(trackId.getId())};
selectionArgs = new String[]{Long.toString(trackId.id())};
}
return getMarkerCursor(null, selection, selectionArgs, MarkerColumns._ID, maxCount);
}
@@ -510,7 +535,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.DESCRIPTION, marker.getDescription());
values.put(MarkerColumns.CATEGORY, marker.getCategory());
values.put(MarkerColumns.ICON, marker.getIcon());
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
values.put(MarkerColumns.TRACKID, marker.getTrackId().id());
values.put(MarkerColumns.LENGTH, marker.getLength().toM());
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
@@ -548,6 +573,35 @@ public class ContentProviderUtils {
return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
public List<Marker> searchMarkers(Track.Id trackId, String query) {
String selection = null;
String[] selectionArgs = null;
String sortOrder = null;
if (query == null) {
if (trackId != null) {
selection = MarkerColumns.TRACKID + " = ?";
selectionArgs = new String[]{Long.toString(trackId.id())};
}
} else {
selection = MarkerColumns.NAME + " LIKE ? OR " +
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.CATEGORY + " LIKE ?";
selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"};
sortOrder = MarkerColumns.DEFAULT_SORT_ORDER + " DESC";
}
ArrayList<Marker> markers = new ArrayList<>();
try (Cursor cursor = getMarkerCursor(null, selection, selectionArgs, sortOrder, -1)) {
if (cursor.moveToFirst()) {
do {
markers.add(createMarker(cursor));
} while (cursor.moveToNext());
}
}
return markers;
}
/**
* Fills a {@link TrackPoint} from a cursor.
*
@@ -632,7 +686,7 @@ public class ContentProviderUtils {
@Deprecated
public TrackPoint.Id getLastTrackPointId(@NonNull Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
String[] selectionArgs = new String[]{Long.toString(trackId.id())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return new TrackPoint.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
@@ -651,7 +705,7 @@ public class ContentProviderUtils {
@Deprecated
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
String[] selectionArgs = new String[]{Long.toString(trackId.id()), Long.toString(location.getTime())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
return new TrackPoint.Id(cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)));
@@ -681,10 +735,10 @@ public class ContentProviderUtils {
String[] selectionArgs;
if (startTrackPointId != null) {
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + ">=?";
selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(startTrackPointId.getId())};
selectionArgs = new String[]{Long.toString(trackId.id()), Long.toString(startTrackPointId.id())};
} else {
selection = TrackPointsColumns.TRACKID + "=?";
selectionArgs = new String[]{Long.toString(trackId.getId())};
selectionArgs = new String[]{Long.toString(trackId.id())};
}
return getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns.DEFAULT_SORT_ORDER);
@@ -699,7 +753,7 @@ public class ContentProviderUtils {
@Deprecated
public TrackPoint getLastValidTrackPoint(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TYPE + " IN (" + TrackPoint.Type.SEGMENT_START_AUTOMATIC.type_db + "," + TrackPoint.Type.TRACKPOINT.type_db + "))";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
String[] selectionArgs = new String[]{Long.toString(trackId.id())};
return findTrackPointBy(selection, selectionArgs);
}
@@ -722,7 +776,7 @@ public class ContentProviderUtils {
*/
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.getId());
values.put(TrackPointsColumns.TRACKID, trackId.id());
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
if (trackPoint.hasLocation()) {
@@ -805,7 +859,7 @@ public class ContentProviderUtils {
public static String formatIdListForUri(Track.Id... trackIds) {
long[] ids = new long[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
ids[i] = trackIds[i].getId();
ids[i] = trackIds[i].id();
}
return formatIdListForUri(ids);
@@ -834,18 +888,20 @@ public class ContentProviderUtils {
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
SensorStatistics sensorStatistics = null;
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null)) {
try (Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.id()), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
final int MAX_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR);
final int AVG_HR_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR);
final int MAX_CADENCE_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE);
final int AVG_CADENCE_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE);
final int MAX_POWER_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_POWER);
final int AVG_POWER_INDEX = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER);
sensorStatistics = new SensorStatistics(
!cursor.isNull(MAX_HR_INDEX) ? HeartRate.of(cursor.getFloat(MAX_HR_INDEX)) : null,
!cursor.isNull(AVG_HR_INDEX) ? HeartRate.of(cursor.getFloat(AVG_HR_INDEX)) : null,
!cursor.isNull(MAX_CADENCE_INDEX) ? Cadence.of(cursor.getFloat(MAX_CADENCE_INDEX)) : null,
!cursor.isNull(AVG_CADENCE_INDEX) ? Cadence.of(cursor.getFloat(AVG_CADENCE_INDEX)) : null,
!cursor.isNull(MAX_POWER_INDEX) ? Power.of(cursor.getFloat(MAX_POWER_INDEX)) : null,
!cursor.isNull(AVG_POWER_INDEX) ? Power.of(cursor.getFloat(AVG_POWER_INDEX)) : null
);
}
@@ -62,7 +62,7 @@ public class CustomContentProvider extends ContentProvider {
/**
* The string representing the query that compute sensor stats from trackpoints table.
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
* It computes the average for heart rate, cadence and power (duration-based average) and the maximum for heart rate, cadence and power.
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
*/
private final String SENSOR_STATS_QUERY =
@@ -86,7 +86,9 @@ public class CustomContentProvider extends ContentProvider {
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
"/ " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + ", " +
"MAX(t." + TrackPointsColumns.SENSOR_POWER + ") " + TrackPointsColumns.ALIAS_MAX_POWER + " " +
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
@@ -177,7 +177,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
ContentValues cv = new ContentValues();
cv.put("uuid", UUIDUtils.toBytes(UUID.randomUUID()));
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.getId())});
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.id())});
} while (cursor.moveToNext());
}
}
@@ -455,7 +455,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
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())});
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.id())});
} while (cursor.moveToNext());
}
}
@@ -86,7 +86,7 @@ public class ShareContentProvider extends CustomContentProvider {
StringBuilder trackIdBuilder = new StringBuilder();
for (Track.Id trackId : trackIds) {
trackIdBuilder.append(trackId.getId()).append(TRACKID_DELIMITER);
trackIdBuilder.append(trackId.id()).append(TRACKID_DELIMITER);
}
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
@@ -199,7 +199,7 @@ public class ShareContentProvider extends CustomContentProvider {
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
Set<Track.Id> trackIds = parseURI(uri);
final ArrayList<Track> tracks = new ArrayList<>();
String[] trackIdsString = trackIds.stream().map(Track.Id::toString).toArray(String[]::new);
String[] trackIdsString = trackIds.stream().map(id -> String.valueOf(id.id())).toArray(String[]::new);
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
try (Cursor cursor = super.query(TracksColumns.CONTENT_URI, null, whereClause, trackIdsString, TracksColumns._ID)) {
@@ -166,7 +166,7 @@ public class TrackDataHub {
public void loadTrack(final @NonNull Track.Id trackId) {
handler.post(() -> {
if (trackId.equals(selectedTrackId)) {
Log.i(TAG, "Not reloading track " + trackId.getId());
Log.i(TAG, "Not reloading track " + trackId.id());
return;
}
selectedTrackId = trackId;
@@ -201,7 +201,7 @@ public class TrackDataHub {
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.trackId());
}
/**
@@ -326,7 +326,7 @@ public class TrackDataHub {
TrackPoint.Id next = null;
if (localLastSeenTrackPointIdId != null) {
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
next = new TrackPoint.Id(localLastSeenTrackPointIdId.id() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
}
TrackPoint trackPoint = null;
@@ -344,7 +344,7 @@ public class TrackDataHub {
TrackPoint.Id trackPointId = trackPoint.getId();
// Stop if past the last wanted point
if (maxPointId != null && trackPointId.getId() > maxPointId.getId()) {
if (maxPointId != null && trackPointId.id() > maxPointId.id()) {
break;
}
@@ -355,7 +355,7 @@ public class TrackDataHub {
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastTrackPointId.getId() - localFirstSeenTrackPointId.getId()); //TODO That is an assumption; should be derived from the DB.
long numTotalPoints = Math.max(0L, lastTrackPointId.id() - localFirstSeenTrackPointId.id()); //TODO That is an assumption; should be derived from the DB.
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
@@ -67,7 +67,7 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
}
// Builds selection arguments.
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.getId())).collect(Collectors.toCollection(ArrayList::new));
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.id())).collect(Collectors.toCollection(ArrayList::new));
args.addAll(categories);
args.addAll(fromToArgs);
selectionArgs = args.stream().toArray(String[]::new);
@@ -17,6 +17,7 @@ public enum ActivityIcon {
UNKNOWN("UNKNOWN", R.drawable.ic_logo_24dp),
WALK("WALK", R.drawable.ic_activity_walk_24dp),
ESCOOTER("ESCOOTER", R.drawable.ic_activity_escooter_24dp),
KICKSCOOTER("KICKSCOOTER", R.drawable.ic_activity_scooter_24dp),
INLINE_SKATING("INLINES_SKATING", R.drawable.ic_activity_inline_skating_24dp),
SKATE_BOARDING("SKATE_BOARDING", R.drawable.ic_activity_skateboarding_24dp),
CLIMBING("CLIMBING", R.drawable.ic_activity_climbing_24dp),
@@ -58,6 +58,7 @@ public enum ActivityType {
ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.UNKNOWN, true),
RUNNING("running", R.string.activity_type_running, ActivityIcon.RUN, false),
SAILING("sailing", R.string.activity_type_sailing, ActivityIcon.SAILING, true),
KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, ActivityIcon.KICKSCOOTER, true),
SEAPLANE("seaplane", R.string.activity_type_seaplane, ActivityIcon.AIRPLANE, true),
SKATE_BOARDING("skate boarding", R.string.activity_type_skate_boarding, ActivityIcon.SKATE_BOARDING, true),
SKATING("skating", R.string.activity_type_skating, ActivityIcon.UNKNOWN, true),
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.data.models;
import java.time.Duration;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
public record Speed(double speed_mps) {
@@ -56,10 +55,6 @@ public record Speed(double speed_mps) {
return Double.isNaN(speed_mps) || Double.isInfinite(speed_mps);
}
public boolean isMoving() {
return !isInvalid() && greaterThan(PreferencesUtils.getIdleSpeed());
}
public boolean lessThan(Speed speed) {
return !greaterThan(speed);
}
@@ -143,53 +143,20 @@ public class Track {
return Objects.hash(id);
}
public static class Id implements Parcelable {
private final long id;
public Id(long id) {
this.id = id;
}
protected Id(Parcel in) {
id = in.readLong();
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id1 = (Id) o;
return id == id1.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public record Id(long id) implements Parcelable {
@Override
public int describeContents() {
return 0;
}
@NonNull
@Override
public String toString() {
return String.valueOf(id);
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(id);
}
public static final Parcelable.Creator<Track.Id> CREATOR = new Parcelable.Creator<>() {
public static final Creator<Track.Id> CREATOR = new Creator<>() {
public Track.Id createFromParcel(Parcel in) {
return new Track.Id(in.readLong());
}
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.data.models;
import android.location.Location;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -25,7 +26,6 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
/**
* Sensor and/or location information for a specific point in time.
@@ -66,6 +66,7 @@ public class TrackPoint {
public final int type_db;
Type(int type_db) {
this.type_db = type_db;
}
@@ -287,11 +288,6 @@ public class TrackPoint {
this.speed = speed;
return this;
}
public boolean isMoving() {
return hasSpeed() && getSpeed().isMoving();
}
public boolean hasBearing() {
return bearing != null;
}
@@ -352,9 +348,8 @@ public class TrackPoint {
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
}
//TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull TrackPoint dest) {
return getLocation().bearingTo(dest.getLocation());
return bearingTo(dest.getLocation());
}
//TODO Bearing requires a location; what do we do if we don't have any?
@@ -449,53 +444,40 @@ public class TrackPoint {
", latitude=" + latitude +
", longitude=" + longitude +
", horizontalAccuracy=" + horizontalAccuracy +
", verticalAccuracy=" + verticalAccuracy +
", altitude=" + altitude +
", speed=" + speed +
", bearing=" + bearing +
", sensorDistance=" + sensorDistance +
", type=" + type +
", heartRate_bpm=" + heartRate +
", cadence_rpm=" + cadence +
", heartRate=" + heartRate +
", cadence=" + cadence +
", power=" + power +
", altitudeGain_m=" + altitudeGain_m +
", altitudeLoss_m=" + altitudeLoss_m +
'}';
}
public static class Id {
public record Id(long id) implements Parcelable {
private final long id;
public Id(long id) {
this.id = id;
}
protected Id(Parcel in) {
id = in.readLong();
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() {
return id;
@Override
public int describeContents() {
return 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TrackPoint.Id id1 = (TrackPoint.Id) o;
return id == id1.id;
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public static final Creator<Id> CREATOR = new Creator<>() {
public Id createFromParcel(Parcel in) {
return new Id(in.readLong());
}
@NonNull
@Override
public String toString() {
return String.valueOf(id);
}
public Id[] newArray(int size) {
return new Id[size];
}
};
}
}
@@ -62,6 +62,7 @@ public interface TrackPointsColumns extends BaseColumns {
String ALIAS_AVG_CADENCE = "avg_cadence";
String ALIAS_MAX_CADENCE = "max_cadence";
String ALIAS_AVG_POWER = "avg_power";
String ALIAS_MAX_POWER = "max_power";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
@@ -47,6 +47,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
ActivityType.SNOW_BOARDING,
ActivityType.SKIING,
ActivityType.WALKING,
ActivityType.KICKSCOOTER,
ActivityType.ESCOOTER,
ActivityType.INLINE_SKATING,
ActivityType.SKATE_BOARDING,
@@ -270,10 +270,12 @@ public class StatisticsRecordedFragment extends Fragment {
viewBinding.statsAvgCadenceValue.setText(avgRPM);
}
if (sensorStatistics.hasPower()) {
String W = String.valueOf(Math.round(sensorStatistics.getAvgPower().getW()));
String maxW = String.valueOf(Math.round(sensorStatistics.getMaxPower().getW()));
String avgW = String.valueOf(Math.round(sensorStatistics.getAvgPower().getW()));
viewBinding.statsPowerGroup.setVisibility(View.VISIBLE);
viewBinding.statsPowerValue.setText(W);
viewBinding.statsMaxPowerValue.setText(maxW);
viewBinding.statsAvgPowerValue.setText(avgW);
}
}
}
@@ -93,7 +93,7 @@ public class TrackFilenameGenerator {
public String getExample() {
Track track = new Track();
track.setName("Berlin");
track.setUuid(UUID.fromString("fefefefefe-0000-1000-8000-00805f9b34fb"));
track.setUuid(UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"));
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
return format(track, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
@@ -340,7 +341,7 @@ public class ExportActivity extends FragmentActivity implements ExportService.Ex
}
@Override
public void onExportError(ExportTask exportTask) {
public void onExportError(ExportTask exportTask, String errorMessage) {
trackExportErrorCount++;
String name;
if (exportTask.isMultiExport()) {
@@ -348,6 +349,7 @@ public class ExportActivity extends FragmentActivity implements ExportService.Ex
} else {
name = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)).getName();
}
Log.e(TAG, "Error exporting " + name + ": " + errorMessage);
trackErrors.add(name);
nextExport(exportTask);
@@ -7,12 +7,12 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.ExportUtils;
public class ExportService extends JobIntentService {
@@ -22,7 +22,6 @@ public class ExportService extends JobIntentService {
private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_EXPORT_TASK = "export_task";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
private static final String TAG = ExportService.class.getSimpleName();
public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) {
Intent intent = new Intent(context, JobService.class);
@@ -46,17 +45,19 @@ public class ExportService extends JobIntentService {
// Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
if (directoryFile == null || !directoryFile.canWrite()) {
Log.e(TAG, "Can't write to directory: " + directoryFile);
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile);
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
return;
}
// Export.
boolean success = ExportUtils.exportTrack(this, directoryFile, exportTask);
// Send result to the receiver.
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
resultReceiver.send(resultCode, bundle);
// Export and send result
try {
ExportUtils.exportTrack(this, directoryFile, exportTask);
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_SUCCESS, bundle);
} catch (Exception e) {
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, e.getMessage());
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
}
}
public static class ExportServiceResultReceiver extends ResultReceiver {
@@ -66,6 +67,8 @@ public class ExportService extends JobIntentService {
public static final String RESULT_EXTRA_EXPORT_TASK = "result_extra_export_task";
public static final String EXTRA_EXPORT_ERROR_MESSAGE = "extra_export_error_message";
private final Receiver receiver;
public ExportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
@@ -75,10 +78,10 @@ public class ExportService extends JobIntentService {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
ExportTask exportTask = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK);
ExportTask exportTask = resultData.getParcelable(RESULT_EXTRA_EXPORT_TASK);
switch (resultCode) {
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
case RESULT_CODE_ERROR -> receiver.onExportError(exportTask);
case RESULT_CODE_ERROR -> receiver.onExportError(exportTask, resultData.getString(EXTRA_EXPORT_ERROR_MESSAGE));
default -> throw new RuntimeException("Unknown resultCode.");
}
}
@@ -87,7 +90,7 @@ public class ExportService extends JobIntentService {
default void onExportSuccess(ExportTask exportTask) {
}
default void onExportError(ExportTask exportTask) {
default void onExportError(ExportTask exportTask, String errorMessage) {
}
}
}
@@ -171,6 +171,9 @@ public class GPXTrackExporter implements TrackExporter {
sensorPoints.add(trackPoint);
}
}
case IDLE -> {
// Not supported as IDLE-TrackPoints have no location.
}
default ->
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
}
@@ -279,8 +282,12 @@ public class GPXTrackExporter implements TrackExporter {
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>");
if (trackStatistics.hasTotalAltitudeGain()) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.getTotalAltitudeGain() + "</gpxtrkx:Ascent>");
}
if (trackStatistics.hasTotalAltitudeLoss()) {
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.getTotalAltitudeLoss() + "</gpxtrkx:Descent>");
}
printWriter.println("</gpxtrkx:TrackStatsExtension>");
printWriter.println("</extensions>");
@@ -61,6 +61,7 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_ACTIVITYTYPE = "type";
public static final String EXTENDED_DATA_TYPE_TRACKPOINT = "trackpoint_type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
@@ -83,6 +84,9 @@ public class KMLTrackExporter implements TrackExporter {
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
private ArrayList<TrackPoint.Type> trackpointTypeList = new ArrayList<>();
private final List<Float> speedList = new ArrayList<>();
private final List<Float> distanceList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>();
@@ -177,12 +181,13 @@ public class KMLTrackExporter implements TrackExporter {
writeCloseSegment();
wroteSegment = false;
}
case TRACKPOINT -> {
case TRACKPOINT, IDLE -> {
if (!wroteSegment) {
// Might happen for older data (pre v3.15.0)
writeOpenSegment();
wroteSegment = true;
}
writeTrackPoint(track.getZoneOffset(), trackPoint);
}
default ->
@@ -309,6 +314,7 @@ public class KMLTrackExporter implements TrackExporter {
@VisibleForTesting
void writeOpenSegment() {
printWriter.println("<Track>");
trackpointTypeList.clear();
speedList.clear();
distanceList.clear();
powerList.clear();
@@ -324,32 +330,35 @@ public class KMLTrackExporter implements TrackExporter {
void writeCloseSegment() {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
writeTrackPointType(trackpointTypeList);
if (speedList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
writeSimpleArraySensorData(speedList, EXTENDED_DATA_TYPE_SPEED);
}
if (distanceList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(distanceList, EXTENDED_DATA_TYPE_DISTANCE);
writeSimpleArraySensorData(distanceList, EXTENDED_DATA_TYPE_DISTANCE);
}
if (powerList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
writeSimpleArraySensorData(powerList, EXTENDED_DATA_TYPE_POWER);
}
if (cadenceList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
writeSimpleArraySensorData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
}
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
writeSimpleArraySensorData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
}
if (altitudeGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
writeSimpleArraySensorData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
}
if (altitudeLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS);
writeSimpleArraySensorData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS);
}
if (accuracyHorizontal.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(accuracyHorizontal, EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL);
writeSimpleArraySensorData(accuracyHorizontal, EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL);
}
if (accuracyVertical.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(accuracyVertical, EXTENDED_DATA_TYPE_ACCURACY_VERTICAL);
writeSimpleArraySensorData(accuracyVertical, EXTENDED_DATA_TYPE_ACCURACY_VERTICAL);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
@@ -360,6 +369,8 @@ public class KMLTrackExporter implements TrackExporter {
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
trackpointTypeList.add(trackPoint.getType());
if (trackPoint.hasLocation()) {
printWriter.println("<coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</coord>");
} else {
@@ -378,13 +389,7 @@ public class KMLTrackExporter implements TrackExporter {
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
}
/**
* Writes the simple array data.
*
* @param list a list of simple array data
* @param name the name of the simple array data
*/
private void writeSimpleArrayData(List<Float> list, String name) {
private void writeSimpleArraySensorData(List<Float> list, String name) {
printWriter.println("<SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
Float value = list.get(i);
@@ -397,14 +402,14 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("</SimpleArrayData>");
}
/**
* Writes a placemark.
*
* @param name the name
* @param activityType the activityType
* @param description the description
* @param location the location
*/
private void writeTrackPointType(List<TrackPoint.Type> list) {
printWriter.println("<SimpleArrayData name=\"" + EXTENDED_DATA_TYPE_TRACKPOINT + "\">");
for (TrackPoint.Type value : list) {
printWriter.println("<value>" + value.name() + "</value>");
}
printWriter.println("</SimpleArrayData>");
}
private void writePlacemark(String name, String activityType, String description, Location location, ZoneOffset zoneOffset) {
if (location != null) {
printWriter.println("<Placemark>");
@@ -94,7 +94,9 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Location> locationList = new ArrayList<>();
private String dataType;
private String dataType; //Could be converted to an ENUM
private final ArrayList<String> trackpointTypeList = new ArrayList<>();
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
@@ -287,6 +289,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
locationList.clear();
whenList.clear();
trackpointTypeList.clear();
sensorSpeedList.clear();
sensorDistanceList.clear();
sensorHeartRateList.clear();
@@ -313,6 +316,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
trackPoint.setLocation(location);
}
if (i < trackpointTypeList.size() && trackpointTypeList.get(i) != null) {
TrackPoint.Type type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
trackPoint.setType(type);
}
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
}
@@ -399,6 +408,10 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
private void onExtendedDataValueEnd() throws SAXException {
if (dataType.equals(KMLTrackExporter.EXTENDED_DATA_TYPE_TRACKPOINT)) {
trackpointTypeList.add(content != null ? content.trim() : null);
return;
}
Float value = null;
if (content != null) {
content = content.trim();
@@ -28,7 +28,6 @@ import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -49,7 +48,7 @@ public class KmzTrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
private static final List<String> KMZ_IMAGES_EXT = List.of("jpeg", "jpg", "png");
private final Context context;
private final TrackImporter trackImporter;
@@ -1,13 +1,14 @@
package de.dennisguse.opentracks.services;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.data.models.Track;
public record RecordingStatus(Track.Id trackId) {
public record RecordingStatus(@Nullable Track.Id trackId) {
public Track.Id getTrackId() {
return trackId;
static RecordingStatus record(@NonNull Track.Id trackId) {
return new RecordingStatus(trackId);
}
public boolean isRecording() {
@@ -18,10 +19,6 @@ public record RecordingStatus(Track.Id trackId) {
return new RecordingStatus(null);
}
static RecordingStatus record(@NonNull Track.Id trackId) {
return new RecordingStatus(trackId);
}
public RecordingStatus stop() {
return TrackRecordingService.STATUS_DEFAULT;
}
@@ -5,10 +5,12 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.ZoneOffset;
@@ -28,19 +30,25 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.TrackNameUtils;
class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChangeListener {
public class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = TrackRecordingManager.class.getSimpleName();
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
private final Runnable ON_IDLE = this::onIdle;
private final ContentProviderUtils contentProviderUtils;
private final Context context;
private final IdleObserver idleObserver;
private final Handler handler;
private final TrackPointCreator trackPointCreator;
private Distance recordingDistanceInterval;
private Distance maxRecordingDistance;
private Duration idleDuration;
private Track.Id trackId;
private TrackStatisticsUpdater trackStatisticsUpdater;
@@ -52,15 +60,17 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
private TrackPoint lastStoredTrackPoint;
private TrackPoint lastStoredTrackPointWithLocation;
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator) {
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, Handler handler) {
this.context = context;
this.idleObserver = idleObserver;
this.trackPointCreator = trackPointCreator;
this.handler = handler;
contentProviderUtils = new ContentProviderUtils(context);
}
Track.Id startNewTrack() {
TrackPoint segmentStartTrackPoint = trackPointCreator.createSegmentStartManual();
// Create new track
ZoneOffset zoneOffset = ZoneOffset.systemDefault().getRules().getOffset(segmentStartTrackPoint.getTime());
Track track = new Track(zoneOffset);
trackId = contentProviderUtils.insertTrack(track);
@@ -74,7 +84,6 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
track.setActivityTypeLocalized(activityTypeLocalized);
track.setActivityType(ActivityType.findByLocalizedString(context, activityTypeLocalized));
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
//TODO Pass TrackPoint
track.setName(TrackNameUtils.getTrackName(context, trackId, track.getStartTime()));
contentProviderUtils.updateTrack(track);
@@ -88,7 +97,7 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
trackId = resumeTrackId;
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.id() + " does not exists.");
return false;
}
@@ -100,7 +109,7 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
return true;
}
void end() {
void endCurrentTrack() {
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPoint(segmentEnd, true);
@@ -154,16 +163,30 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
return new Marker.Id(ContentUris.parseId(uri));
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void onIdle() {
Log.d(TAG, "Becoming idle");
onNewTrackPoint(trackPointCreator.createIdle());
idleObserver.onIdle();
}
/**
* @return TrackPoint was stored?
*/
boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
if (trackPoint.hasSpeed()) {
lastTrackPointUIWithSpeed = trackPoint;
}
if (trackPoint.hasAltitude()) {
lastTrackPointUIWithAltitude = trackPoint;
}
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
insertTrackPoint(trackPoint, true);
handler.removeCallbacks(ON_IDLE);
return true;
}
//Storing trackPoint
// Always insert the first segment location
@@ -184,10 +207,10 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
if (!shouldStore) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance (and sensor data is not new enough).");
return false;
} else {
insertTrackPoint(trackPoint, true);
return true;
}
insertTrackPoint(trackPoint, true);
return true;
}
Distance distanceToLastStoredTrackPoint;
@@ -200,18 +223,17 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
insertTrackPoint(trackPoint, true);
handler.removeCallbacks(ON_IDLE);
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
return true;
}
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)
&& trackPoint.isMoving()) {
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPoint(trackPoint, false);
return true;
}
if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
// Moving from non-moving to moving or vice versa; required to compute moving time correctly.
insertTrackPoint(trackPoint, true);
handler.removeCallbacks(ON_IDLE);
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
return true;
}
@@ -252,6 +274,7 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
}
} catch (SQLiteException e) {
// TODO Remove; if this is a problem; use a synchronized method.
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
* This is expected to happen extremely rarely (if our listener gets invoked twice at about the same time).
@@ -277,5 +300,12 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
if (PreferencesUtils.isKey(R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
}
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
idleDuration = PreferencesUtils.getIdleDurationTimeout();
}
}
public interface IdleObserver {
void onIdle();
}
}
@@ -32,6 +32,8 @@ import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Distance;
@@ -45,7 +47,7 @@ import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener {
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
private static final String TAG = TrackRecordingService.class.getSimpleName();
@@ -105,8 +107,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
trackPointCreator = new TrackPointCreator(this, this, handler);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator);
trackPointCreator = new TrackPointCreator(this);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this , handler);
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -212,7 +214,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// Set recording status
updateRecordingStatus(STATUS_DEFAULT);
trackRecordingManager.end();
trackRecordingManager.endCurrentTrack();
stopUpdateRecordingData();
@@ -244,6 +246,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
Log.e(TAG, "newGpsStatus: " + gpsStatusValue.message);
if (notificationManager == null) {
StringWriter writer = new StringWriter();
Exception e = new RuntimeException("TrackRecording.newGpsStatus() called after onDestroy(); objectID: " + this + " with thread: " + Thread.currentThread().toString());
e.printStackTrace(new PrintWriter(writer));
Log.e(TAG, e.getMessage() + " " + writer);
return;
}
notificationManager.updateContent(getString(gpsStatusValue.message));
gpsStatusObservable.postValue(gpsStatusValue);
}
@@ -285,11 +296,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
// Compute temporary track statistics using sensorData and update time.
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.getDataForUI();
voiceAnnouncementManager.update(this, data.first);
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first);
recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second));
}
public void onIdle() {
voiceAnnouncementManager.announceIdle();
}
@VisibleForTesting
public void stopUpdateRecordingData() {
handler.removeCallbacks(updateRecordingData);
@@ -27,42 +27,23 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
/**
* This class will announce the user's {@link TrackStatistics}.
*
* @author Sandor Dornbush
*/
public class VoiceAnnouncement {
public class TTSManager {
public final static int AUDIO_STREAM = TextToSpeech.Engine.DEFAULT_STREAM;
private static final String TAG = VoiceAnnouncement.class.getSimpleName();
private static final String TAG = TTSManager.class.getSimpleName();
private final Context context;
private final AudioManager audioManager;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -109,12 +90,9 @@ public class VoiceAnnouncement {
private MediaPlayer ttsFallback;
VoiceAnnouncement(Context context) {
TTSManager(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start() {
@@ -140,7 +118,7 @@ public class VoiceAnnouncement {
}
}
public void announce(@NonNull Track track) {
public void announce(@NonNull Spannable announcement) {
synchronized (this) {
if (!ttsReady) {
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
@@ -150,7 +128,7 @@ public class VoiceAnnouncement {
}
}
if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
if (List.of(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
.contains(audioManager.getMode())) {
Log.i(TAG, "Announcement is not allowed at this time.");
return;
@@ -167,23 +145,6 @@ public class VoiceAnnouncement {
return;
}
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
if (announcement.length() > 0) {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
@@ -17,6 +17,7 @@ package de.dennisguse.opentracks.services.announcement;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.Spannable;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -27,11 +28,15 @@ import androidx.mediarouter.media.MediaRouter;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
/**
* Execute a periodic task on a time or distance schedule.
@@ -42,9 +47,9 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
private static final String TAG = VoiceAnnouncementManager.class.getSimpleName();
private final TrackRecordingService trackRecordingService;
private final Context context;
private VoiceAnnouncement voiceAnnouncement;
private TTSManager voiceAnnouncement;
private TrackStatistics trackStatistics;
@@ -58,12 +63,22 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
@NonNull
private Duration nextTotalTime = TOTALTIME_OFF;
public VoiceAnnouncementManager(@NonNull TrackRecordingService trackRecordingService) {
this.trackRecordingService = trackRecordingService;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
public VoiceAnnouncementManager(@NonNull Context context) {
this.context = context;
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start(@Nullable TrackStatistics trackStatistics) {
voiceAnnouncement = new VoiceAnnouncement(trackRecordingService);
voiceAnnouncement = new TTSManager(context);
voiceAnnouncement.start();
update(trackStatistics);
}
@@ -74,10 +89,10 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
updateNextTaskDistance();
}
public void update(@NonNull Context context, @NonNull Track track) {
private boolean shouldNotAnnounce() {
if (voiceAnnouncement == null) {
Log.e(TAG, "Cannot update when in status shutdown.");
return;
return true;
}
if (!PreferencesUtils.shouldVoiceAnnouncementOnDeviceSpeaker()
@@ -85,6 +100,26 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
.getSelectedRoute()
.isDeviceSpeaker()) {
Log.i(TAG, "No voice announcement on device speaker.");
return true;
}
return false;
}
public void announceIdle() {
if (shouldNotAnnounce()) {
return;
}
if (!PreferencesUtils.shouldVoiceAnnouncementIdle()) {
return;
}
voiceAnnouncement.announce(VoiceAnnouncementUtils.createIdle(context));
}
public void announceStatisticsIfNeeded(@NonNull Track track) {
if (shouldNotAnnounce()) {
return;
}
@@ -100,10 +135,29 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
}
if (announce) {
voiceAnnouncement.announce(track);
voiceAnnouncement.announce(createAnnouncement(track));
}
}
private Spannable createAnnouncement(Track track) {
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
return VoiceAnnouncementUtils.createStatistics(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics);
}
public void stop() {
if (voiceAnnouncement != null) {
voiceAnnouncement.stop();
@@ -33,7 +33,12 @@ class VoiceAnnouncementUtils {
private VoiceAnnouncementUtils() {
}
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
static Spannable createIdle(Context context) {
return new SpannableStringBuilder()
.append(context.getString(R.string.voiceIdle));
}
static Spannable createStatistics(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
SpannableStringBuilder builder = new SpannableStringBuilder();
Distance totalDistance = trackStatistics.getTotalDistance();
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
@@ -36,16 +36,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
private Clock clock = new MonotonicClock();
private SensorManager sensorManager;
public TrackPointCreator(Callback service, Context context, Handler handler) {
public TrackPointCreator(Callback service) {
this.service = service;
this.sensorManager = new SensorManager(this);
}
@VisibleForTesting
TrackPointCreator(Callback service) {
this.service = service;
}
public synchronized void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
@@ -106,6 +101,13 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
return segmentEnd;
}
public synchronized TrackPoint createIdle() {
TrackPoint idle = new TrackPoint(TrackPoint.Type.IDLE, createNow());
addSensorData(idle);
reset();
return idle;
}
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastTrackPointUISpeed, @Nullable TrackPoint lastTrackPointUIAltitude, @Nullable TrackPoint lastStoredTrackPointWithLocation) {
TrackPoint currentTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow());
@@ -17,7 +17,7 @@ public class AnnouncementsSettingsFragment extends PreferenceFragmentCompat {
@Override
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_announcements_title);
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_announcements_statistics_title);
}
@Override
@@ -79,7 +79,7 @@ public class GpsSettingsFragment extends PreferenceFragmentCompat {
ListPreference recordingGpsAccuracy = findPreference(getString(R.string.recording_gps_accuracy_key));
recordingGpsAccuracy.setEntries(PreferencesUtils.getThresholdHorizontalAccuracyEntries());
ListPreference idleSpeed = findPreference(getString(R.string.idle_speed_key));
idleSpeed.setEntries(PreferencesUtils.getIdleSpeedEntries());
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries());
}
}
@@ -45,7 +45,6 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.HeartRateZones;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
@@ -313,6 +312,10 @@ public class PreferencesUtils {
return getBoolean(R.string.voice_on_device_speaker_key, DEFAULT);
}
public static boolean shouldVoiceAnnouncementIdle() {
return getBoolean(R.string.voice_announce_idle_key, true);
}
public static Duration getVoiceAnnouncementFrequency() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_frequency_default));
int value = getInt(R.string.voice_announcement_frequency_key, DEFAULT);
@@ -530,11 +533,12 @@ public class PreferencesUtils {
static String[] getMinRecordingIntervalEntries() {
String[] entryValues = resources.getStringArray(R.array.min_recording_interval_values);
long recommended = PreferencesUtils.getMinRecordingIntervalDefault().getSeconds();
String[] entries = new String[entryValues.length];
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
if (value == PreferencesUtils.getMinRecordingIntervalDefault().getSeconds()) {
if (value == recommended) {
entries[i] = resources.getString(R.string.value_smallest_recommended);
} else {
entries[i] = value < 60 ? resources.getString(R.string.value_integer_second, value) : resources.getString(R.string.value_integer_minute, value / 60);
@@ -598,49 +602,25 @@ public class PreferencesUtils {
return entries;
}
public static Speed getIdleSpeed() {
final float DEFAULT = Float.parseFloat(resources.getString(R.string.idle_speed_default));
float value = getFloat(R.string.idle_speed_key, DEFAULT);
return Speed.ofKMH(value);
public static Duration getIdleDurationTimeout() {
final int DEFAULT = Integer.parseInt(resources.getString(R.string.idle_duration_default));
int value = getInt(R.string.idle_duration_key, DEFAULT);
return Duration.ofSeconds(value);
}
static String[] getIdleSpeedEntries() {
String[] entryValues = resources.getStringArray(R.array.idle_speed_values);
static String[] getIdleDurationEntries() {
String[] entryValues = resources.getStringArray(R.array.idle_duration_values);
String[] entries = new String[entryValues.length];
final float idleSpeedDefault = Float.parseFloat(resources.getString(R.string.idle_speed_default));
UnitSystem unitSystem = getUnitSystem();
final int idleDurationDefault = Integer.parseInt(resources.getString(R.string.idle_duration_default));
for (int i = 0; i < entryValues.length; i++) {
float value = Float.parseFloat(entryValues[i]);
int value = Integer.parseInt(entryValues[i]);
switch (unitSystem) {
case METRIC -> {
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
} else {
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
}
}
case IMPERIAL_FEET, IMPERIAL_METER -> {
double valueMPH = Speed.ofKMH(value).toMPH();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
} else {
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
}
}
case NAUTICAL_IMPERIAL -> {
double valueKnots = Speed.ofKMH(value).toKnots();
if (value == idleSpeedDefault) {
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
} else {
entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
}
}
default -> throw new RuntimeException("Not implemented");
if (value == idleDurationDefault) {
entries[i] = resources.getString(R.string.value_int_seconds, value);
} else {
entries[i] = value < 60 ? resources.getString(R.string.value_integer_second, value) : resources.getString(R.string.value_integer_minute, value / 60);
}
}
@@ -16,8 +16,9 @@ import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
public static final String EXTRAS_CHECK_EXPORT_DIRECTORY = "Check Export Directory";
private boolean checkExportDirectory = false;
public static final String EXTRAS_EXPORT_ERROR_MESSAGE = "Export error message";
private String exportErrorMessage = null;
public static final String FRAGMENT_KEY = "fragmentKey";
@@ -30,8 +31,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null && intent.hasExtra(EXTRAS_CHECK_EXPORT_DIRECTORY)) {
checkExportDirectory = true;
if (intent != null) {
exportErrorMessage = intent.getStringExtra(EXTRAS_EXPORT_ERROR_MESSAGE);
}
if (savedInstanceState != null) {
@@ -48,15 +49,15 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
@Override
protected void onResume() {
if (checkExportDirectory) {
checkExportDirectory = false;
if (exportErrorMessage != null) {
new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_logo_24dp)
.setTitle(R.string.app_name)
.setMessage(R.string.export_error_post_workout)
.setMessage(getString(R.string.export_error_post_workout) + "\n" + exportErrorMessage)
.setNeutralButton(android.R.string.ok, null)
.create()
.show();
exportErrorMessage = null;
}
super.onResume();
@@ -50,7 +50,7 @@ public class ShareUtils {
for (Track.Id trackId : trackIds) {
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "TrackId " + trackId.getId() + " could not be resolved.");
Log.e(TAG, "TrackId " + trackId.id() + " could not be resolved.");
continue;
}
@@ -9,13 +9,15 @@ public class SensorStatistics {
private final HeartRate avgHr;
private final Cadence maxCadence;
private final Cadence avgCadence;
private final Power maxPower;
private final Power avgPower;
public SensorStatistics(HeartRate maxHr, HeartRate avgHr, Cadence maxCadence, Cadence avgCadence, Power avgPower) {
public SensorStatistics(HeartRate maxHr, HeartRate avgHr, Cadence maxCadence, Cadence avgCadence, Power maxPower, Power avgPower) {
this.maxHr = maxHr;
this.avgHr = avgHr;
this.maxCadence = maxCadence;
this.avgCadence = avgCadence;
this.maxPower = maxPower;
this.avgPower = avgPower;
}
@@ -47,6 +49,10 @@ public class SensorStatistics {
return avgPower != null;
}
public Power getMaxPower() {
return maxPower;
}
public Power getAvgPower() {
return avgPower;
}
@@ -61,6 +61,8 @@ public class TrackStatistics {
// The average heart rate seen on this track
private HeartRate avgHeartRate = null;
private boolean isIdle;
public TrackStatistics() {
reset();
}
@@ -81,6 +83,7 @@ public class TrackStatistics {
totalAltitudeGain_m = other.totalAltitudeGain_m;
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
avgHeartRate = other.avgHeartRate;
isIdle = other.isIdle;
}
@VisibleForTesting
@@ -168,6 +171,8 @@ public class TrackStatistics {
setMaxSpeed(Speed.zero());
setTotalAltitudeGain(null);
setTotalAltitudeLoss(null);
isIdle = false;
}
public void reset(Instant startTime) {
@@ -248,6 +253,14 @@ public class TrackStatistics {
return totalTime.minus(movingTime);
}
public boolean isIdle() {
return isIdle;
}
public void setIdle(boolean idle) {
isIdle = idle;
}
public boolean hasAverageHeartRate() {
return avgHeartRate != null;
}
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.stats;
import android.util.Log;
import androidx.annotation.NonNull;
import java.time.Duration;
@@ -40,11 +38,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
public class TrackStatisticsUpdater {
private static final String TAG = TrackStatisticsUpdater.class.getSimpleName();
/**
* Ignore any acceleration faster than this.
* Will ignore any speeds that imply acceleration greater than 2g's
*/
private static final double SPEED_MAX_ACCELERATION = 2 * 9.81;
private final TrackStatistics trackStatistics;
@@ -132,26 +125,35 @@ public class TrackStatisticsUpdater {
currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM));
}
// Update total distance
if (trackPoint.hasSensorDistance()) {
// Sensor-based distance/speed
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
} 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);
currentSegment.addTotalDistance(movingDistance);
}
{
// Update total distance
Distance movingDistance = null;
if (trackPoint.hasSensorDistance()) {
movingDistance = trackPoint.getSensorDistance();
} else if (lastTrackPoint != null
&& lastTrackPoint.hasLocation()
&& trackPoint.hasLocation()) {
// GPS-based distance/speed
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
}
if (movingDistance != null) {
currentSegment.setIdle(false);
currentSegment.addTotalDistance(movingDistance);
}
if (!currentSegment.isIdle() && !trackPoint.isSegmentManualStart()) {
if (lastTrackPoint != null) {
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
}
}
// Update moving time
if (trackPoint.isMoving() && lastTrackPoint != null && lastTrackPoint.isMoving()) {
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
currentSegment.setIdle(true);
}
// Update max speed
updateSpeed(trackPoint, lastTrackPoint);
if (trackPoint.hasSpeed()) {
updateSpeed(trackPoint);
}
}
if (trackPoint.isSegmentManualEnd()) {
@@ -180,27 +182,13 @@ public class TrackStatisticsUpdater {
/**
* Updates a speed reading while assuming the user is moving.
*/
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (isValidSpeed(trackPoint, lastTrackPoint)) {
Speed currentSpeed = trackPoint.getSpeed();
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(currentSpeed);
}
} else {
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
private void updateSpeed(@NonNull TrackPoint trackPoint) {
Speed currentSpeed = trackPoint.getSpeed();
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(currentSpeed);
}
}
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
Speed maxSpeedDifference = Speed.of(Distance.of(SPEED_MAX_ACCELERATION), Duration.ofMillis(1000))
.mul(timeDifference.toSeconds());
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
return speedDifference.lessThan(maxSpeedDifference);
}
@NonNull
@Override
public String toString() {
@@ -0,0 +1,294 @@
package de.dennisguse.opentracks.ui;
import android.app.ActivityOptions;
import android.content.Intent;
import android.database.Cursor;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListItemBinding;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
public class TrackListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ActionMode.Callback {
private static final String TAG = TrackListAdapter.class.getSimpleName();
private final AppCompatActivity context;
private final RecyclerView recyclerView;
private final SparseBooleanArray selection = new SparseBooleanArray();
private RecordingStatus recordingStatus;
private UnitSystem unitSystem;
private Cursor tracks;
private boolean selectionMode = false;
private ActivityUtils.ContextualActionModeCallback actionModeCallback;
private ActionMode actionMode;
public TrackListAdapter(AppCompatActivity context, RecyclerView recyclerView, RecordingStatus recordingStatus, UnitSystem unitSystem) {
this.context = context;
this.recyclerView = recyclerView;
this.recordingStatus = recordingStatus;
this.unitSystem = unitSystem;
}
public void setActionModeCallback(ActivityUtils.ContextualActionModeCallback actionModeCallback) {
this.actionModeCallback = actionModeCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.track_list_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ViewHolder viewHolder = (ViewHolder) holder;
tracks.moveToPosition(position);
viewHolder.bind(tracks);
}
@Override
public int getItemCount() {
if (tracks == null) {
return 0;
}
return tracks.getCount();
}
public void swapData(Cursor tracks) {
this.tracks = tracks;
this.notifyDataSetChanged();
}
public void updateRecordingStatus(RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
}
public void updateUnitSystem(UnitSystem unitSystem) {
this.unitSystem = unitSystem;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
selectionMode = true;
actionModeCallback.onPrepare(menu, getCheckedPositions(), getCheckedIds(), true);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (actionModeCallback.onClick(item.getItemId(), getCheckedPositions(), getCheckedIds())) {
mode.finish();
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
selectionMode = false;
setAllSelected(false);
selection.clear();
actionModeCallback.onDestroy();
}
public void setAllSelected(boolean isSelected) {
for (int i = 0; i < getItemCount(); i++) {
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(i);
holder.setSelected(isSelected);
}
}
private int[] getCheckedPositions() {
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < selection.size(); i++) {
if (selection.valueAt(i)) {
positions.add(selection.keyAt(i));
}
}
return positions.stream().mapToInt(i -> i).toArray();
}
private long[] getCheckedIds() {
List<Long> ids = new ArrayList<>();
for (int i = 0; i < selection.size(); i++) {
if (selection.valueAt(i)) {
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(selection.keyAt(i));
ids.add(holder.getId());
}
}
return ids.stream().mapToLong(i -> i).toArray();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
private final TrackListItemBinding viewBinding;
private final View view;
private ActivityType activityType;
private String name;
private int markerCount;
private Duration totalTime;
private Distance totalDistance;
private Instant startTime;
private ZoneOffset zoneOffset;
private String activityTypeLocalized;
private String description;
private Track.Id trackId;
public ViewHolder(@NonNull View itemView) {
super(itemView);
viewBinding = TrackListItemBinding.bind(itemView);
view = itemView;
view.setOnClickListener(this);
view.setOnLongClickListener(this);
}
private void getData(Cursor track) {
final int idIndex = track.getColumnIndexOrThrow(TracksColumns._ID);
final int nameIndex = track.getColumnIndexOrThrow(TracksColumns.NAME);
final int descriptionIndex = track.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
final int activityTypeLocalizedIndex = track.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED);
final int startTimeIndex = track.getColumnIndexOrThrow(TracksColumns.STARTTIME);
final int startTimeOffsetIndex = track.getColumnIndexOrThrow(TracksColumns.STARTTIME_OFFSET);
final int totalDistanceIndex = track.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
final int totalTimeIndex = track.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
final int iconIndex = track.getColumnIndexOrThrow(TracksColumns.ICON);
final int markerCountIndex = track.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
activityType = ActivityType.findBy(track.getString(iconIndex));
name = track.getString(nameIndex);
markerCount = track.getInt(markerCountIndex);
totalTime = Duration.ofMillis(track.getLong(totalTimeIndex));
totalDistance = Distance.of(track.getFloat(totalDistanceIndex));
startTime = Instant.ofEpochMilli(track.getLong(startTimeIndex));
zoneOffset = ZoneOffset.ofTotalSeconds(track.getInt(startTimeOffsetIndex));
activityTypeLocalized = track.getString(activityTypeLocalizedIndex);
description = track.getString(descriptionIndex);
trackId = new Track.Id(track.getLong(idIndex));
}
public void bind(Cursor track) {
getData(track);
int iconId = activityType.getIconDrawableId();
int iconDesc = R.string.image_track;
boolean isRecordingThisTrackRecording = trackId.equals(recordingStatus.trackId());
if (isRecordingThisTrackRecording) {
iconId = R.drawable.ic_track_recording;
iconDesc = R.string.image_record;
}
viewBinding.trackListItemIcon.setImageResource(iconId);
viewBinding.trackListItemIcon.setContentDescription(context.getString(iconDesc));
viewBinding.trackListItemName.setText(name);
String timeDistanceText = ListItemUtils.getTimeDistanceText(context, unitSystem, isRecordingThisTrackRecording, totalTime, totalDistance, markerCount);
viewBinding.trackListItemTimeDistance.setText(timeDistanceText);
viewBinding.trackListItemMarkerCountIcon.setVisibility(markerCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.trackListItemMarkerCount.setText(markerCount > 0 ? Integer.toString(markerCount) : null);
if (!recordingStatus.isRecording()) {
ListItemUtils.setDateAndTime(context, viewBinding.trackListItemDate, viewBinding.trackListItemTime, startTime, zoneOffset);
} else {
viewBinding.trackListItemDate.setText(null);
viewBinding.trackListItemTime.setText(null);
}
String category = activityType == null ? activityTypeLocalized : null;
String categoryDescription = StringUtils.getCategoryDescription(category, description);
viewBinding.trackListItemCategoryDescription.setText(categoryDescription);
viewBinding.trackListItemCategoryDescription.setVisibility(categoryDescription.equals("") ? View.GONE : View.VISIBLE);
setSelected(selection.get(getLayoutPosition()));
}
public void setSelected(boolean isSelected) {
selection.put(getLayoutPosition(), isSelected);
view.setActivated(isSelected);
}
public long getId() {
return trackId.id();
}
@Override
public void onClick(View v) {
if (selectionMode) {
setSelected(!view.isActivated());
actionMode.invalidate();
return;
}
if (recordingStatus.isRecording() && trackId.equals(recordingStatus.trackId())) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
context.startActivity(newIntent);
} else {
// Not recording -> open detail activity.
Intent newIntent = IntentUtils.newIntent(context, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
context,
new Pair<>(view.findViewById(R.id.track_list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON));
context.startActivity(newIntent, activityOptions.toBundle());
}
}
@Override
public boolean onLongClick(View v) {
actionMode = context.startSupportActionMode(TrackListAdapter.this);
setSelected(true);
return true;
}
}
}
@@ -18,19 +18,16 @@ package de.dennisguse.opentracks.ui.markers;
import android.app.SearchManager;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.AbstractActivity;
@@ -38,15 +35,12 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.databinding.MarkerListBinding;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.markers.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ScrollVisibleViews;
import de.dennisguse.opentracks.util.IntentUtils;
/**
@@ -54,23 +48,20 @@ import de.dennisguse.opentracks.util.IntentUtils;
*
* @author Leif Hendrik Wilden
*/
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
public class MarkerListActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
private static final String TAG = MarkerListActivity.class.getSimpleName();
private ContentProviderUtils contentProviderUtils;
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter;
private Track.Id trackId;
private MarkerListAdapter adapter;
private MarkerListBinding viewBinding;
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
@@ -105,33 +96,24 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private MenuItem insertMarkerMenuItem;
private MenuItem searchMenuItem;
private String searchQuery;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
contentProviderUtils = new ContentProviderUtils(this);
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
resourceCursorAdapter.markerInvalid(id);
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
startActivity(intent);
});
resourceCursorAdapter = new MarkerResourceCursorAdapter(this, R.layout.list_item);
ScrollVisibleViews scrollVisibleViews = new ScrollVisibleViews(resourceCursorAdapter);
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
viewBinding.markerList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
adapter = new MarkerListAdapter(this, viewBinding.markerList, null);
viewBinding.markerList.setLayoutManager(layoutManager);
viewBinding.markerList.setAdapter(adapter);
setSupportActionBar(viewBinding.markerListToolbar);
adapter.setActionModeCallback(contextualActionModeCallback);
}
@Override
@@ -145,7 +127,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
super.onResume();
trackRecordingServiceConnection.bind(this);
this.invalidateOptionsMenu();
loadData(getIntent());
loadData();
}
@Override
@@ -157,9 +139,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
protected void onDestroy() {
super.onDestroy();
resourceCursorAdapter.clear();
viewBinding = null;
resourceCursorAdapter = null;
adapter = null;
contentProviderUtils = null;
}
@@ -183,15 +164,15 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()));
insertMarkerMenuItem.setVisible(trackId != null && trackId.equals(recordingStatus.trackId()));
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
if (trackId != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
@@ -229,7 +210,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
if (itemId == R.id.list_context_menu_edit) {
if (markerIds.length == 1) {
resourceCursorAdapter.markerInvalid(markerIds[0].id());
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
@@ -243,23 +223,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
viewBinding.markerList.setItemChecked(i, true);
}
adapter.setAllSelected(true);
return false;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
@@ -267,8 +237,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
if (searchQuery != null) {
searchQuery = null;
loadData();
return;
}
@@ -276,78 +247,33 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
public void onNewIntent(Intent intent) {
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
} else {
searchQuery = null;
}
loaderCallbacks.setSearch(searchQuery);
}
@Override
public void onMarkerDeleted() {
// Do nothing
private void loadData() {
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
List<Marker> markers = contentProviderUtils.searchMarkers(trackId, searchQuery);
adapter.swapData(markers);
if (markers.isEmpty()) {
viewBinding.markerListEmpty.setVisibility(View.VISIBLE);
viewBinding.markerList.setVisibility(View.GONE);
} else {
viewBinding.markerListEmpty.setVisibility(View.GONE);
viewBinding.markerList.setVisibility(View.VISIBLE);
}
}
private void onRecordingStatusChanged(RecordingStatus status) {
recordingStatus = status;
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
}
public void restart() {
LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
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.TRACKID};
if (searchQuery == null) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().getId())}, null);
} else {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
}
} else {
final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " +
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC");
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
}
@@ -0,0 +1,276 @@
package de.dennisguse.opentracks.ui.markers;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.util.Log;
import android.util.LruCache;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.RecyclerView;
import java.io.IOException;
import java.io.InputStream;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.databinding.MarkerListItemBinding;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ExecutorListViewService;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.ui.util.ThemeUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
public class MarkerListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ActionMode.Callback {
private static final String TAG = MarkerListAdapter.class.getSimpleName();
private final AppCompatActivity context;
private final RecyclerView recyclerView;
private List<Marker> markers;
private final SparseBooleanArray selection;
private boolean selectionMode;
private ActivityUtils.ContextualActionModeCallback actionModeCallback;
private ActionMode actionMode;
private final ExecutorListViewService<Marker.Id> executorService = new ExecutorListViewService<>(128);
private final LruCache<Marker.Id, Bitmap> memoryCache;
public MarkerListAdapter(AppCompatActivity context, RecyclerView recyclerView, List<Marker> markers) {
this.context = context;
this.recyclerView = recyclerView;
this.markers = markers;
selection = new SparseBooleanArray();
selectionMode = false;
// Get max available VM memory, exceeding this amount will throw an OutOfMemory exception.
final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8;
memoryCache = new LruCache<>(cacheSize) {
@Override
protected int sizeOf(Marker.Id key, Bitmap value) {
return value.getByteCount();
}
};
}
public void setActionModeCallback(ActivityUtils.ContextualActionModeCallback actionModeCallback) {
this.actionModeCallback = actionModeCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.marker_list_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ViewHolder viewHolder = (ViewHolder) holder;
viewHolder.bind(markers.get(position));
}
@Override
public int getItemCount() {
if (markers == null) {
return 0;
}
return markers.size();
}
public void swapData(List<Marker> markers) {
this.markers = markers;
this.notifyDataSetChanged();
}
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
actionMode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
selectionMode = true;
actionModeCallback.onPrepare(menu, getCheckedPositions(), getCheckedIds(), true);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
if (actionModeCallback.onClick(menuItem.getItemId(), getCheckedPositions(), getCheckedIds())) {
actionMode.finish();
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
selectionMode = false;
setAllSelected(false);
selection.clear();
actionModeCallback.onDestroy();
}
public void setAllSelected(boolean isSelected) {
for (int i = 0; i < getItemCount(); i++) {
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(i);
holder.setSelected(isSelected);
}
}
private int[] getCheckedPositions() {
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < selection.size(); i++) {
if (selection.valueAt(i)) {
positions.add(selection.keyAt(i));
}
}
return positions.stream().mapToInt(i -> i).toArray();
}
private long[] getCheckedIds() {
List<Long> ids = new ArrayList<>();
for (int i = 0; i < selection.size(); i++) {
if (selection.valueAt(i)) {
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(selection.keyAt(i));
ids.add(holder.getId());
}
}
return ids.stream().mapToLong(i -> i).toArray();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
private final MarkerListItemBinding viewBinding;
private final View view;
private Marker marker;
public ViewHolder(@NonNull View view) {
super(view);
viewBinding = MarkerListItemBinding.bind(view);
this.view = view;
view.setOnClickListener(this);
view.setOnLongClickListener(this);
}
public void bind(Marker marker) {
this.marker = marker;
viewBinding.markerListItemPhoto.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE);
viewBinding.markerListItemTextGradient.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE);
if (marker.hasPhoto()) {
int height = ThemeUtils.getPhotoHeight(context);
ViewGroup.LayoutParams params = viewBinding.markerListItemPhoto.getLayoutParams();
params.height = height;
viewBinding.markerListItemPhoto.setLayoutParams(params);
viewBinding.markerListItemTextGradient.setLayoutParams(params);
asyncLoadPhoto(viewBinding.markerListItemPhoto, marker.getPhotoURI(), marker.getId());
}
viewBinding.markerListItemName.setText(marker.getName());
{
ZoneOffset timeZone = new ContentProviderUtils(context).getTrack(marker.getTrackId()).getZoneOffset();
ListItemUtils.setDateAndTime(context, viewBinding.markerListItemDate, viewBinding.markerListItemTime, marker.getTime(), timeZone);
}
String categoryDescription = StringUtils.getCategoryDescription(marker.getCategory(), marker.getDescription());
viewBinding.markerListItemTimeDistance.setText(categoryDescription);
setSelected(selection.get(getLayoutPosition()));
}
public void setSelected(boolean isSelected) {
selection.put(getLayoutPosition(), isSelected);
view.setActivated(isSelected);
}
public long getId() {
return marker.getId().id();
}
@Override
public void onClick(View view) {
if (selectionMode) {
setSelected(!view.isActivated());
actionMode.invalidate();
} else {
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, marker.getId());
context.startActivity(intent);
}
}
@Override
public boolean onLongClick(View view) {
actionMode = context.startSupportActionMode(MarkerListAdapter.this);
setSelected(true);
return true;
}
}
/**
* It loads the photoUrl in the imageView from view.
* It takes the photo from cache or from storage if isn't in the cache.
*
* @param imageView view object where photo will be loaded.
* @param photoUri photo's url.
* @param id marker's id where photo belong.
*/
private void asyncLoadPhoto(ImageView imageView, Uri photoUri, Marker.Id id) {
Bitmap bm = memoryCache.get(id);
imageView.setImageBitmap(bm);
if (bm != null) {
return;
}
executorService.execute(id, () -> {
try (InputStream inputStream = context.getContentResolver().openInputStream(photoUri)) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
int height = ThemeUtils.getPhotoHeight(context);
Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height);
final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height);
context.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto));
memoryCache.put(id, finalPhoto);
} catch (IOException e) {
Log.e(TAG, "Failed to image " + e);
}
});
}
}
@@ -1,195 +0,0 @@
package de.dennisguse.opentracks.ui.markers;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.collection.LruCache;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.OffsetDateTime;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.ui.util.ExecutorListViewService;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.ui.util.ScrollVisibleViews;
public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implements ScrollVisibleViews.VisibleViewsListener {
private static final String TAG = MarkerResourceCursorAdapter.class.getSimpleName();
private static final int LIST_PREFERRED_ITEM_HEIGHT_DEFAULT = 128;
private final Activity activity;
//TODO Should be Marker.Id
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
private boolean scroll = false;
// Cache size is in bytes.
private final LruCache<String, Bitmap> memoryCache;
public MarkerResourceCursorAdapter(Activity activity, int layout) {
super(activity, layout, null, 0);
this.activity = activity;
// Get max available VM memory, exceeding this amount will throw an OutOfMemory exception.
final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8;
memoryCache = new LruCache<>(cacheSize) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap bitmap) {
return bitmap.getByteCount();
}
};
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndex(MarkerColumns._ID);
int nameIndex = cursor.getColumnIndex(MarkerColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
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;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
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));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
ImageView imageView = view.findViewById(R.id.list_item_photo);
ImageView textGradient = view.findViewById(R.id.list_item_text_gradient);
imageView.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
imageView.setImageBitmap(null);
if (hasPhoto) {
int height = getPhotoHeight(activity);
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.height = height;
imageView.setLayoutParams(params);
if (getBitmapFromMemCache(String.valueOf(id)) != null || !scroll) {
asyncLoadPhoto(view, imageView, photoUrl, id);
}
}
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
Track track = contentProviderUtils.getTrack(new Track.Id(trackId));
ListItemUtils.setListItem(activity, view, false, iconId, R.string.image_marker, name, null, null, 0,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), track.getZoneOffset()), category, description, hasPhoto);
}
public void clear() {
executorService.shutdown();
}
@Override
public void onViewVisible(View view, int position) {
scroll = true;
Cursor cursor = getCursor();
if (!cursor.moveToPosition(position)) {
return;
}
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
if (hasPhoto) {
ImageView imageView = view.findViewById(R.id.list_item_photo);
asyncLoadPhoto(view, imageView, photoUrl, id);
}
}
public void markerInvalid(long id) {
memoryCache.remove(String.valueOf(id));
scroll = false;
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
synchronized (memoryCache) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap);
}
}
}
private Bitmap getBitmapFromMemCache(String key) {
return memoryCache.get(key);
}
/**
* It loads the photoUrl in the imageView from view.
* It takes the photo from cache or from storage if isn't in the cache.
*
* @param view item's view.
* @param imageView view object where photo will be loaded.
* @param photoUrl photo's url.
* @param id marker's id where photo belong.
*/
private void asyncLoadPhoto(View view, ImageView imageView, String photoUrl, long id) {
Bitmap photo = getBitmapFromMemCache(String.valueOf(id));
imageView.setImageBitmap(photo);
if (photo == null) {
executorService.execute(id, () -> {
try (InputStream inputStream = activity.getContentResolver().openInputStream(Uri.parse(photoUrl))) {
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
int height = getPhotoHeight(activity);
Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height);
final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height);
addBitmapToMemoryCache(String.valueOf(id), finalPhoto);
if (view.getTag().equals(String.valueOf(id))) {
activity.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto));
}
} catch (IOException e) {
Log.e(TAG, "Failed to image " + e);
}
});
}
}
/**
* Gets the photo height.
*
* @param context the context
*/
private static int getPhotoHeight(Context context) {
int[] attrs = new int[]{android.R.attr.listPreferredItemHeight};
TypedArray typeArray = context.obtainStyledAttributes(attrs);
int height = typeArray.getDimensionPixelSize(0, LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
typeArray.recycle();
return 2 * height;
}
}
@@ -18,16 +18,20 @@ package de.dennisguse.opentracks.ui.util;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -40,101 +44,6 @@ public class ListItemUtils {
private ListItemUtils() {
}
/**
* Sets a list item.
*
* @param context the context
* @param view the list item view
* @param isRecording true if recording
* @param iconId the icon id
* @param iconContentDescriptionId the icon content description id
* @param name the name value
* @param totalTime the total time value
* @param totalDistance the total distance value
* @param markerCount the marker count
* @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, 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 = R.drawable.ic_track_recording;
iconContentDescriptionId = R.string.image_record;
}
ImageView iconImageView = view.findViewById(R.id.list_item_icon);
iconImageView.setImageResource(iconId);
iconImageView.setContentDescription(context.getString(iconContentDescriptionId));
// Set name
TextView nameTextView = view.findViewById(R.id.list_item_name);
setTextView(context, nameTextView, name, hasPhoto);
// Set totalTime/totalDistance
TextView timeDistanceTextView = view.findViewById(R.id.list_item_time_distance);
String timeDistanceText;
if (isRecording) {
timeDistanceText = context.getString(R.string.generic_recording);
} else {
// Match list_item_time_distance in list_item.xml
timeDistanceText = getTimeDistance(totalTime, totalDistance);
if (markerCount > 0) {
timeDistanceText += " ‧";
}
}
setTextView(context, timeDistanceTextView, timeDistanceText, hasPhoto);
// Set markerCount
ImageView markerCountIcon = view.findViewById(R.id.list_item_marker_count_icon);
TextView markerCountTextView = view.findViewById(R.id.list_item_marker_count);
boolean hasMarker = markerCount > 0;
markerCountIcon.setVisibility(hasMarker ? View.VISIBLE : View.GONE);
String markerCountValue = hasMarker ? String.valueOf(markerCount) : null;
if (hasMarker) {
// Scale markerCountIcon
int lineHeight = markerCountTextView.getLineHeight();
LayoutParams layoutParams = markerCountIcon.getLayoutParams();
layoutParams.width = lineHeight;
layoutParams.height = lineHeight;
}
setTextView(context, markerCountTextView, markerCountValue, hasPhoto);
// Set date/time
TextView dateTextView = view.findViewById(R.id.list_item_date);
TextView timeTextView = view.findViewById(R.id.list_item_time);
String dateValue = null;
String timeValue = null;
if (!isRecording) {
dateValue = StringUtils.formatDateTodayRelative(context, offsetDateTime);
String pattern = "HH:mm";
if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) {
pattern = "HH:mm x";
}
timeValue = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern));
}
setTextView(context, dateTextView, dateValue, hasPhoto);
setTextView(context, timeTextView, timeValue, hasPhoto);
// Set category and description
TextView categoryDescriptionTextView = view.findViewById(R.id.list_item_category_description);
String categoryDescription = isRecording ? null : StringUtils.getCategoryDescription(category, description);
// Place categoryDescription in either ownerTimeDistanceTextView or categoryDescriptionTextView
if (timeDistanceTextView.getVisibility() == View.GONE && markerCountIcon.getVisibility() == View.GONE) {
setTextView(context, categoryDescriptionTextView, null, hasPhoto);
// Match list_item_category_description in list_item.xml
timeDistanceTextView.setSingleLine(false);
timeDistanceTextView.setMaxLines(2);
setTextView(context, timeDistanceTextView, categoryDescription, hasPhoto);
} else {
// Match list_item_time_distance in list_item.xml
timeDistanceTextView.setSingleLine(true);
setTextView(context, categoryDescriptionTextView, categoryDescription, hasPhoto);
}
}
/**
* Gets a string for share owner, total time, and total distance.
*
@@ -158,6 +67,29 @@ public class ListItemUtils {
return builder.toString();
}
public static String getTimeDistanceText(Context context, UnitSystem unitSystem, boolean isRecording, Duration totalTime, Distance totalDistance, int markerCount) {
String timeDistanceText;
if (isRecording) {
timeDistanceText = context.getString(R.string.generic_recording);
} else {
// Match list_item_time_distance in list_item.xml
String time = StringUtils.formatElapsedTime(totalTime);
// TODO unit system
String distance = DistanceFormatter.Builder()
.setUnit(unitSystem)
.build(context)
.formatDistance(totalDistance);
timeDistanceText = getTimeDistance(time, distance);
if (markerCount > 0) {
timeDistanceText += " ‧";
}
}
return timeDistanceText;
}
/**
* Sets a text view.
*
@@ -180,4 +112,17 @@ public class ListItemUtils {
textView.setShadowLayer(0, 0, 0, 0);
}
}
public static void setDateAndTime(Context context, TextView dateView, TextView timeView, Instant time, ZoneOffset timeZone) {
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(time, timeZone);
String dateContent = StringUtils.formatDateTodayRelative(context, offsetDateTime);
String pattern = "HH:mm";
if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) {
pattern = "HH:mm x";
}
String timeContent = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern));
dateView.setText(dateContent);
timeView.setText(timeContent);
}
}
@@ -52,4 +52,15 @@ public class ThemeUtils {
typedArray.recycle();
return fontSize;
}
/**
* Gets the photo height.
*/
public static int getPhotoHeight(Context context) {
int[] attrs = new int[]{android.R.attr.listPreferredItemHeight};
TypedArray typeArray = context.obtainStyledAttributes(attrs);
int height = typeArray.getDimensionPixelSize(0, 128);
typeArray.recycle();
return 2 * height;
}
}
@@ -8,7 +8,6 @@ import android.net.Uri;
import android.os.Handler;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Toast;
import androidx.documentfile.provider.DocumentFile;
@@ -19,7 +18,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -40,17 +38,12 @@ public class ExportUtils {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
DocumentFile directory = IntentUtils.toDocumentFile(context, PreferencesUtils.getDefaultExportDirectoryUri());
if (directory == null || !directory.canWrite()) {
Toast.makeText(context, R.string.export_cannot_write_to_dir, Toast.LENGTH_LONG).show();
return;
}
ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() {
@Override
public void onExportError(ExportTask unused) {
public void onExportError(ExportTask unused, String errorMessage) {
Intent intent = new Intent(context, SettingsActivity.class);
intent.putExtra(SettingsActivity.EXTRAS_CHECK_EXPORT_DIRECTORY, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, errorMessage);
context.startActivity(intent);
}
});
@@ -59,7 +52,7 @@ public class ExportUtils {
}
}
public static boolean exportTrack(Context context, DocumentFile directory, ExportTask exportTask) {
public static void exportTrack(Context context, DocumentFile directory, ExportTask exportTask) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Track> tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList());
Uri exportDocumentFileUri;
@@ -71,27 +64,21 @@ public class ExportUtils {
}
if (exportDocumentFileUri == null) {
Log.e(TAG, "Couldn't create document file for export");
return false;
throw new RuntimeException("Couldn't create document file for export");
}
TrackExporter trackExporter = exportTask.getTrackFileFormat().createTrackExporter(context, contentProviderUtils);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
if (trackExporter.writeTrack(tracks, outputStream)) {
return true;
} else {
if (!trackExporter.writeTrack(tracks, outputStream)) {
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
Log.e(TAG, "Unable to delete exportDocumentFile");
throw new RuntimeException("Unable to delete exportDocumentFile");
}
Log.e(TAG, "Unable to export track");
return false;
throw new RuntimeException("Unable to export track");
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFileUri, e);
return false;
throw new RuntimeException("Unable to open exportDocumentFile " + exportDocumentFileUri, e);
} catch (IOException e) {
Log.e(TAG, "Unable to close exportDocumentFile output stream", e);
return false;
throw new RuntimeException("Unable to close exportDocumentFile output stream", e);
}
}
@@ -59,7 +59,7 @@ public class FileUtils {
}
public static File getPhotoDir(Context context, Track.Id trackId) {
File photoDirectory = new File(getPhotoDir(context), "" + trackId.getId());
File photoDirectory = new File(getPhotoDir(context), "" + trackId.id());
photoDirectory.mkdirs();
return photoDirectory;
}
@@ -60,11 +60,12 @@ public class PermissionRequester {
private static final List<String> GPS_PERMISSION;
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION);
} else {
//TODO ACCESS_BACKGROUND_LOCATION is required for API, but the permission is not properly granted. See #1653.
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION);
// } else {
GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
}
// }
}
private static final List<String> BLUETOOTH_PERMISSIONS;
@@ -44,7 +44,7 @@ public class TrackNameUtils {
} else if (trackName.equals(context.getString(R.string.settings_recording_track_name_date_iso_8601_value))) {
return startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmX"));
} else {
return context.getString(R.string.track_name_format, trackId.getId());
return context.getString(R.string.track_name_format, trackId.id());
}
}
}
@@ -0,0 +1,239 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="5.8208"
android:viewportHeight="5.8208">
<path
android:pathData="m-1.3253,1.5033 l-0.4101,0.6834a0.1285,0.1285 60.4819,0 0,0.1102 0.1945l0.2496,-0 -0.11,0.5496a0.0898,0.0898 21.1422,0 0,0.1651 0.0638L-0.9105,2.3112A0.1285,0.1285 60.4819,0 0,-1.0206 2.1167H-1.2702L-1.1602,1.5671a0.0898,0.0898 21.1422,0 0,-0.1651 -0.0638z"
android:strokeLineJoin="round"
android:strokeWidth="0.264583"
android:fillColor="#fdbc4b"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M14.7722,-15.5564l26.5377,0l0,30.6204l-26.5377,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.6652"
android:fillColor="#cacaca"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,-14.5358l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#e74c3c"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8755,-14.5358l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#ed1515"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,-14.5358l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#f47750"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,-14.5358l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#fdbc4b"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,-14.5358l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#c9ce3b"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,-10.453l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#da4453"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8755,-10.453l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#c0392b"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,-10.453l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#d35400"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,-10.453l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#f67400"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,-10.453l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#f39c1f"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#1cdc9a"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8755,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#2ecc71"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#1abc9c"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#3498db"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#3daee9"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#10d116"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8755,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#27ae60"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#16a085"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#2980b9"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#1d99f3"
android:strokeLineCap="square"/>
<path
android:pathData="M36.2064,-4.329l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#9b59b6"
android:strokeLineCap="square"/>
<path
android:pathData="M36.2064,-0.2463l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#8e44ad"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,5.8778l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#34495e"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8756,5.8778l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#31363b"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,5.8778l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#7f8c8d"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,5.8778l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#bdc3c7"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,5.8778l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#fcfcfc"
android:strokeLineCap="square"/>
<path
android:pathData="M15.7928,9.9605l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#2c3e50"
android:strokeLineCap="square"/>
<path
android:pathData="M19.8756,9.9605l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#232629"
android:strokeLineCap="square"/>
<path
android:pathData="M23.9583,9.9605l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#4d4d4d"
android:strokeLineCap="square"/>
<path
android:pathData="M28.041,9.9605l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#95a5a6"
android:strokeLineCap="square"/>
<path
android:pathData="M32.1237,9.9605l4.0827,0l0,4.0827l-4.0827,0z"
android:strokeLineJoin="round"
android:strokeWidth="5.7692"
android:fillColor="#eff0f1"
android:strokeLineCap="square"/>
<path
android:pathData="M3.7026,1.1906A0.1323,0.1323 0,0 0,3.5858 1.264A0.1323,0.1323 0,0 0,3.6453 1.4413L3.9445,1.5911C4.0556,1.6468 4.1371,1.748 4.1672,1.8686L4.6013,3.6044C4.0697,3.6801 3.6613,4.0946 3.5987,4.6302L2.2221,4.6302C2.1532,4.0395 1.668,3.5724 1.0594,3.5719C1.0537,3.5718 1.048,3.5718 1.0423,3.5719L0.5292,3.5719A0.1323,0.1323 0,0 0,0.3969 3.7042A0.1323,0.1323 0,0 0,0.5292 3.8365L1.0423,3.8365A0.1323,0.1323 0,0 0,1.0433 3.8365C1.048,3.8364 1.0526,3.8364 1.0573,3.8365A0.1323,0.1323 0,0 0,1.0583 3.8365C1.5713,3.8365 1.9844,4.2495 1.9844,4.7625A0.1323,0.1323 0,0 0,2.1167 4.8948L3.7042,4.8948A0.1323,0.1323 0,0 0,3.8365 4.7625C3.8365,4.2495 4.2495,3.8365 4.7625,3.8365A0.1323,0.1323 0,0 0,4.8907 3.6721L4.4235,1.8045C4.3747,1.6092 4.2433,1.4446 4.0633,1.3544A0.1323,0.1323 0,0 0,4.0628 1.3544L3.7631,1.2046A0.1323,0.1323 0,0 0,3.7026 1.1906z"
android:strokeLineJoin="round"
android:strokeWidth="0.265"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillType="nonZero"
android:strokeLineCap="round"/>
<path
android:pathData="M4.7617,4.1367C4.4266,4.1367 4.1523,4.4109 4.1523,4.7461C4.1523,5.0812 4.4266,5.3555 4.7617,5.3555C5.0969,5.3555 5.3711,5.0812 5.3711,4.7461C5.3711,4.4109 5.0969,4.1367 4.7617,4.1367zM4.7617,4.3379C4.9879,4.3379 5.168,4.5199 5.168,4.7461C5.168,4.9723 4.9879,5.1543 4.7617,5.1543C4.5355,5.1543 4.3535,4.9723 4.3535,4.7461C4.3535,4.5199 4.5355,4.3379 4.7617,4.3379z"
android:strokeAlpha="0.997"
android:strokeLineJoin="round"
android:strokeWidth="0.201567"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillType="nonZero"
android:fillAlpha="0.997"
android:strokeLineCap="butt"/>
<path
android:pathData="m1.0222,4.1367c-0.3352,0 -0.6094,0.2742 -0.6094,0.6094 0,0.3352 0.2742,0.6094 0.6094,0.6094 0.3352,-0 0.6094,-0.2742 0.6094,-0.6094 -0,-0.3352 -0.2742,-0.6094 -0.6094,-0.6094zM1.0222,4.3379c0.2262,0 0.4062,0.182 0.4063,0.4082 -0,0.2262 -0.18,0.4082 -0.4063,0.4082 -0.2262,0 -0.4082,-0.182 -0.4082,-0.4082 0,-0.2262 0.182,-0.4082 0.4082,-0.4082z"
android:strokeAlpha="0.997"
android:strokeLineJoin="round"
android:strokeWidth="0.201567"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillType="nonZero"
android:fillAlpha="0.997"
android:strokeLineCap="butt"/>
</vector>
-146
View File
@@ -1,146 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/activated_background"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<!-- Photo -->
<ImageView
android:id="@+id/list_item_photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="4dp"
android:contentDescription="@string/image_photo_marker"
android:scaleType="centerCrop" />
<!-- A gradient on top of the text -->
<ImageView
android:id="@+id/list_item_text_gradient"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:alpha=".6"
android:visibility="gone"
tools:visibility="visible"
android:contentDescription="@string/image_photo_marker"
android:src="@drawable/text_gradient" />
<!-- Icon -->
<ImageView
android:id="@+id/list_item_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:contentDescription="@string/image_track"
tools:src="@drawable/ic_activity_bike_24dp"/>
<!-- Name -->
<TextView
android:id="@+id/list_item_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/textAppearanceBody1"
android:singleLine="true"
android:layout_marginStart="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/list_item_icon"
app:layout_constraintEnd_toStartOf="@id/list_item_date"
tools:text="Main text" />
<!-- Date -->
<TextView
android:id="@+id/list_item_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/textAppearanceBodySmall"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/list_item_name"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="end"
android:layout_marginStart="8dp"
tools:text="2022-02-17" />
<!-- Total time and distance -->
<TextView
android:id="@+id/list_item_time_distance"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/textAppearanceBodySmall"
app:layout_constraintTop_toBottomOf="@id/list_item_name"
app:layout_constraintStart_toStartOf="@id/list_item_name"
tools:text="50:43 (50.00km)" />
<!-- Marker count icon -->
<ImageView
android:id="@+id/list_item_marker_count_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/list_item_name"
app:layout_constraintStart_toEndOf="@id/list_item_time_distance"
app:layout_constraintBottom_toBottomOf="@id/list_item_time_distance"
android:contentDescription="@string/image_marker_count"
android:scaleType="centerInside"
android:src="@drawable/ic_marker_show_24dp" />
<!-- Marker count text -->
<TextView
android:id="@+id/list_item_marker_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/textAppearanceBodySmall"
app:layout_constraintStart_toEndOf="@id/list_item_marker_count_icon"
app:layout_constraintBottom_toBottomOf="@id/list_item_marker_count_icon"
android:singleLine="true"
tools:text="2" />
<!-- Time -->
<TextView
android:id="@+id/list_item_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/textAppearanceBodySmall"
app:layout_constraintTop_toBottomOf="@id/list_item_name"
app:layout_constraintStart_toEndOf="@id/list_item_marker_count"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="end"
android:layout_gravity="bottom"
android:layout_marginStart="8dp"
tools:text="10:10:10"/>
<!-- Second row barrier -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/list_item_second_row_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="list_item_time_distance,list_item_marker_count_icon,list_item_marker_count,list_item_time" />
<!-- Category and description -->
<TextView
android:id="@+id/list_item_category_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="?attr/textAppearanceBodySmall"
app:layout_constraintTop_toBottomOf="@id/list_item_second_row_barrier"
app:layout_constraintStart_toStartOf="@id/list_item_name"
app:layout_constraintEnd_toEndOf="parent"
android:maxLines="2"
android:ellipsize="end"
tools:text="Description" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -37,7 +37,7 @@ limitations under the License.
android:paddingBottom="?attr/actionBarSize"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/marker_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
+96
View File
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/activated_background"
android:padding="8dp">
<!-- Photo -->
<ImageView
android:id="@+id/marker_list_item_photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/image_photo_marker"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@id/marker_list_item_bottom_line"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<!-- A gradient on top of the text -->
<ImageView
android:id="@+id/marker_list_item_text_gradient"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha=".6"
android:contentDescription="@string/image_photo_marker"
android:src="@drawable/text_gradient"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/marker_list_item_bottom_line"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- Name -->
<TextView
android:id="@+id/marker_list_item_name"
style="?attr/textAppearanceBody1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Main text" />
<!-- Date -->
<TextView
android:id="@+id/marker_list_item_date"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="2022-02-17" />
<!-- Total time and distance -->
<TextView
android:id="@+id/marker_list_item_time_distance"
style='?attr/textAppearanceBodySmall'
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/marker_list_item_name"
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_name"
tools:text="50:43 (50.00km)" />
<!-- Time -->
<TextView
android:id="@+id/marker_list_item_time"
style="?attr/textAppearanceBodySmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_name"
tools:text="10:10:10" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/marker_list_item_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="marker_list_item_photo,marker_list_item_text_gradient,marker_list_item_name,marker_list_item_date,marker_list_item_time_distance,marker_list_item_time" />
<!-- Bottom Line -->
<View
android:id="@+id/marker_list_item_bottom_line"
style="@style/HorizontalLine"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_barrier" />
</androidx.constraintlayout.widget.ConstraintLayout>

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