Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3075f4010f | |||
| 82e305e6a7 | |||
| 6351a335c0 | |||
| 6538daedef | |||
| 82fe859c84 | |||
| f567b03be5 | |||
| 7dc4e8dbc8 | |||
| 2a272a887e | |||
| 868509e00b | |||
| 477bdd5214 | |||
| 286cec8d77 | |||
| 58d4ee7d7d | |||
| 0e93a304f0 | |||
| 0adc4c7986 | |||
| a93424629d | |||
| da29766fd0 | |||
| dc7afbd07f | |||
| 67b174feac | |||
| b6b81b5d3d | |||
| 7d804b603d | |||
| 8696a15ce7 | |||
| 989fe39849 | |||
| ed1e432691 | |||
| 4f612e32d6 | |||
| e5d1e421a4 | |||
| 08ab8cc61d | |||
| c3b30815c8 | |||
| 5a4919efd2 | |||
| bb549fdc93 | |||
| 73e59cbba2 | |||
| 27727b6dcf | |||
| 06d2957407 | |||
| 8b08af6bcf | |||
| 78dad95ef6 | |||
| be9d56721e | |||
| e5785dd3ac | |||
| 11d912d14d | |||
| 807ac3fc9f | |||
| 45874d3749 | |||
| 594f69320a | |||
| d4927c9444 | |||
| 00dbd056d6 | |||
| 8f19ad8f66 | |||
| 2fdad54a7c | |||
| 5a5f040ed5 | |||
| 0ada9a2463 | |||
| 719bb981ec | |||
| 3c95d8744d | |||
| 63d1c9ee5e | |||
| 1ccd2e3c0a | |||
| cdbddabc1b | |||
| 9f6ca83f27 | |||
| f2ee99a477 | |||
| 7009ede095 | |||
| 2b530a1c53 | |||
| 33673f5921 | |||
| a7398d43e3 | |||
| 59b016a4a3 | |||
| 78aa5d51c6 | |||
| 95f584b554 | |||
| cf759d526d | |||
| 4c44ea12a4 | |||
| 9d09865907 | |||
| 5667d2d83e | |||
| 386f017fb7 | |||
| 87594e822d | |||
| ee832c7bd2 | |||
| 30042d5bc3 | |||
| 73cbb7a29c | |||
| c100fdde4f | |||
| 04e1496084 | |||
| 21d1852505 | |||
| 1502b0cfa7 | |||
| e7d6f65508 | |||
| 0e69a444a5 | |||
| 3b62999fde | |||
| 97431233ac | |||
| d49675afc8 | |||
| c1f7ab36a8 | |||
| eb14cc7bc6 | |||
| a47b9164af | |||
| 502e3ecb21 | |||
| 71903f174a | |||
| ab6606a295 | |||
| c4e2023992 | |||
| cb10bdda3f | |||
| f913472aae | |||
| af4826c578 | |||
| 63172ec66f | |||
| 3d6c339509 | |||
| 49f86c4eb9 | |||
| 7664bd248d | |||
| 73615aa0d6 | |||
| 81bb6699d8 | |||
| 93c67e3728 | |||
| 993816a34a | |||
| 3c0999c3a0 | |||
| 4528e7148f | |||
| 74042f5d85 | |||
| 92f6fbe04a | |||
| 5fa50250de | |||
| 8ee9daf7af | |||
| a54c8c1d25 | |||
| 8137ec8de2 | |||
| ae2a97576b | |||
| 3e830a086e | |||
| 618a1df3ea | |||
| 90cfc67c59 | |||
| cecc586868 | |||
| f956e7fab3 | |||
| 128b95cd83 | |||
| 895a5c2b2a | |||
| 91ce5cf8d7 | |||
| a283464331 | |||
| 1c3a2bd2e4 | |||
| 0b326a77c2 | |||
| d68008941e | |||
| b822d05a78 | |||
| 74bfed768c | |||
| ebfcce6d8d | |||
| d97d8f85e4 | |||
| 37c2c5029e | |||
| 105ff89fd8 | |||
| 0ac033106c | |||
| 21ec9fc568 | |||
| 2e3a7a31d4 | |||
| ecd7405a8c | |||
| 28abe360cb | |||
| 9cf88c00b0 | |||
| 8bbde4aacd | |||
| 00351136f2 | |||
| 30b85bd215 | |||
| 14b705b43e | |||
| 31fc0adabc | |||
| 1714da5d3b | |||
| 6ae47b5e01 | |||
| df8e4c2e2e | |||
| 6f9fa3719a | |||
| b9014c7219 | |||
| d44ab44620 | |||
| bcffe05744 | |||
| ed60734868 | |||
| e314ff8f43 | |||
| 1108684d06 | |||
| 6c0033c107 | |||
| 8a3e50ef9b | |||
| 05b572253e | |||
| f755a086cd | |||
| b0605323da | |||
| 585c2234eb | |||
| c5d7ea4245 | |||
| 281cd8a861 | |||
| 36400c8ad0 | |||
| fb00f697c5 | |||
| f529726218 | |||
| 059f3d8f43 | |||
| cae716925f | |||
| d29ee6da3d | |||
| 033b5211ea | |||
| 5f4f5baba4 | |||
| 4421046556 | |||
| 30482d5a34 | |||
| f12094c953 | |||
| c5147e7664 | |||
| 03b66bc6a2 | |||
| 7b40f2c949 | |||
| 785a922d03 | |||
| 5d2414977b | |||
| f9b8e46b32 | |||
| c79fc2f353 | |||
| e283e993bb | |||
| 3f1e74e71e | |||
| 300453e3e6 | |||
| ac8eea377e | |||
| cc44e8e9bf | |||
| 3860472e6b | |||
| 69029486b6 | |||
| b407f86d5e | |||
| 731adc7e31 | |||
| 858149e718 | |||
| 34e796f73c | |||
| 3ac71361b8 | |||
| e409b315b4 | |||
| 24eb768707 | |||
| 17ad13d559 | |||
| 3b71212905 | |||
| 16a29c5e31 | |||
| e421164282 | |||
| de01157964 | |||
| 8ef51ec318 | |||
| d2139d70ad | |||
| d1a37366be | |||
| 52d7c8a6cb | |||
| c62e56e5a3 | |||
| 6b89092adf | |||
| 118a56b8d6 | |||
| 7fb993014c | |||
| 09f72af66c | |||
| 815f111ca6 | |||
| 40d2bf0c22 | |||
| a77b7a0626 | |||
| 1a00b4b779 | |||
| 6c5c47240c | |||
| 3c32da970b | |||
| 9cff62c5ba | |||
| 424f93b4da | |||
| 3a23a412c8 | |||
| 9d64989ef1 | |||
| da62d26409 | |||
| a350077e5c | |||
| 36b6cd3c98 | |||
| 203b457ad0 | |||
| 0873b3bd12 |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Data not recorded
|
||||
about: Somehow OpenTracks did not record GPS data
|
||||
title: ''
|
||||
labels: potential bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please describe the problem and how to reproduce it:**
|
||||
|
||||
...
|
||||
|
||||
**Checklist**
|
||||
|
||||
- [ ] Consulted [https://dontkillmyapp.com/]
|
||||
- [ ] Power management: battery optimization is disabled for OpenTracks
|
||||
|
||||
**Technical information**
|
||||
|
||||
- Device: [e.g. Nexus 7 (2013)]
|
||||
- OS: [e.g. LineageOS 15.1]
|
||||
- OpenTracks version: [e.g. v3.2.4]
|
||||
- OpenTracks commit id (only for nightly builds): [e.g. 4b333bb]
|
||||
@@ -1,9 +1,10 @@
|
||||
#https://github.com/marketplace/actions/android-emulator-runner
|
||||
name: Build nightly
|
||||
on: [ push ]
|
||||
name: Build APK
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
nightly-build:
|
||||
apk-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
@@ -14,7 +15,7 @@ jobs:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
|
||||
- name: Assemble nighlty APK
|
||||
- name: Assemble APK
|
||||
shell: bash
|
||||
run: |
|
||||
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
|
||||
@@ -28,7 +29,7 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: OpenTracks-nightly.apk
|
||||
name: OpenTracks-APK.apk
|
||||
path: ./build/outputs/apk/nightly/*.apk
|
||||
retention-days: 7
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Test
|
||||
on:
|
||||
push:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
java-version: '11'
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.24.0
|
||||
uses: reactivecircus/android-emulator-runner@v2.25.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
@@ -9,7 +9,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
||||
<th>Free (F-Droid)</th>
|
||||
<th>Free (Nightly for F-Droid)</th>
|
||||
<th>Donations</th>
|
||||
<th>Translations</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
@@ -27,47 +26,26 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
||||
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
|
||||
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/svg-badge.svg" alt="Translation status" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
|
||||
</td>
|
||||
<td></td>
|
||||
<td align="center">
|
||||
<a href="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc">
|
||||
<img alt="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc" src="promotion/donations-bitcoin.png" height="90" align="middle">
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td align="center">
|
||||
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
|
||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td align="center">
|
||||
<a href="https://www.amazon.de/dp/B09K2KNQ7X/">
|
||||
<img alt="Get it on Amazon App Store" src="https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-appstore-badge-english-white.png" height="40" align="middle">
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
|
||||
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/horizontal-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div>
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.png">
|
||||
<img width="23%" src="fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.png">
|
||||
@@ -86,19 +64,19 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
||||
* __Voice announcements__
|
||||
* __Photos and Markers:__ mark interesting locations while tracking
|
||||
* __Export:__
|
||||
* export tracks either as KMZ (incl. photos), KML, or GPX
|
||||
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* export tracks either as [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) (incl. photos), [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html), or [GPX 1.1](https://www.topografix.com/GPX/1/1/)
|
||||
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* __Altitude:__
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* __Bluetooth LE sensors:__
|
||||
* heart rate
|
||||
* cycling: speed and distance
|
||||
* cycling: cadence
|
||||
* cycling: power meter
|
||||
* running: speed and cadence
|
||||
* support for BLE sensor training only (i.e., without GPS) for indoor training
|
||||
* heart rate
|
||||
* cycling: speed and distance
|
||||
* cycling: cadence
|
||||
* cycling: power meter
|
||||
* running: speed and cadence
|
||||
* support for BLE sensor training only (i.e., without GPS) for indoor training
|
||||
|
||||
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
|
||||
|
||||
@@ -117,7 +95,7 @@ OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
|
||||
__Only required permission:__
|
||||
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
||||
|
||||
### API
|
||||
### Public API
|
||||
|
||||
OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)).
|
||||
The API can be invoked by sending an explicit Intent to start an activity.
|
||||
@@ -129,19 +107,42 @@ The API can be invoked by sending an explicit Intent to start an activity.
|
||||
* Nightly: `de.dennisguse.opentracks.nightly`
|
||||
|
||||
`Class`:
|
||||
|
||||
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
|
||||
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
||||
|
||||
For testing via adb: `adb shell am start -n "package/class"`
|
||||
For testing via adb: `adb shell am start -e someParameter someValue -n "package/class"`
|
||||
|
||||
The Start/Stop API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
|
||||
`StartRecording` supports the following parameters:
|
||||
|
||||
## Custom Dashboards (incl. map)
|
||||
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live tracks.
|
||||
* Set track data: `TRACK_NAME`, `TRACK_DESCRIPTION`, `TRACK_CATEGORY`, and `TRACK_ICON` (non-localized identifier see [/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38](/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38)).
|
||||
NOTE: if `TRACK_ICON` is not present, `TRACK_CATEGORY` will be used to determine the icon (localized).
|
||||
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE` and `STATS_TARGET_CLASS`
|
||||
|
||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track statistics on supported wearables.
|
||||
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
|
||||
|
||||
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
|
||||
However, this is rather slow and does not provide updates while recording.
|
||||
## 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) |
|
||||
|
||||
## Dashboard API (incl. map)
|
||||
|
||||
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live
|
||||
tracks.
|
||||
|
||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which
|
||||
presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also
|
||||
used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track
|
||||
statistics on supported wearables.
|
||||
|
||||
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (
|
||||
e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)). However, this is rather
|
||||
slow and does not provide updates while recording.
|
||||
|
||||
The developer documentation is in [README_API.md](README_API.md).
|
||||
|
||||
|
||||
@@ -38,10 +38,6 @@ However, often only one value is provided.
|
||||
|
||||
We only support _Instantaneous Power_.
|
||||
|
||||
_WARNING_: we only support physically movements with valid GPS coordinates.
|
||||
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
|
||||
Also the distance is not computed.
|
||||
|
||||
For cycling:
|
||||
* Assioma Duo Powermeter
|
||||
* Elite drivo II
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
# Folder where svg screenshots are expected.
|
||||
SRC_DIR="screenshots-svg/"
|
||||
# Folder where png files will be saved.
|
||||
DST_DIR="fastlane/metadata/android/en-US/phoneScreenshots/"
|
||||
DST_DIR="fastlane/metadata/android/en-US/images/phoneScreenshots/"
|
||||
|
||||
echo "Exporting svg files to png..."
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
compileSdkVersion 31
|
||||
compileSdk 33
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
@@ -59,13 +59,13 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4816
|
||||
versionName "v3.27.0"
|
||||
versionCode 5042
|
||||
versionName "v4.1.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
@@ -81,6 +81,8 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'de.dennisguse.opentracks'
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
@@ -100,6 +102,7 @@ android {
|
||||
releasePlayStore {
|
||||
applicationIdSuffix ".playstore"
|
||||
versionNameSuffix "-PlayStore"
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,20 +120,23 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.5.1'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.8.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
@@ -1,37 +1,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
enable-background="new 0 0 95.334 100"
|
||||
height="100"
|
||||
id="Ebene_1"
|
||||
version="1.1"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px"
|
||||
xml:space="preserve"><metadata
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
enable-background="new 0 0 95.334 100"
|
||||
height="100"
|
||||
id="Ebene_1"
|
||||
version="1.1"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
x="0px"
|
||||
y="0px"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="LOGO.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview10"
|
||||
showgrid="false"
|
||||
inkscape:zoom="7.94"
|
||||
inkscape:cx="50"
|
||||
inkscape:cy="50"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Ebene_1" /><metadata
|
||||
id="metadata16"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
|
||||
<defs id="defs14" />
|
||||
<g id="g9"
|
||||
transform="translate(-1.0593197,1.9067775)">
|
||||
<linearGradient gradientUnits="userSpaceOnUse"
|
||||
id="SVGID_1_"
|
||||
x1="28.7554"
|
||||
x2="82.789497"
|
||||
y1="81.535202"
|
||||
y2="9.2979002">
|
||||
<stop style="stop-color:#FBB22F"
|
||||
id="stop2"
|
||||
offset="0" />
|
||||
<stop style="stop-color:#F58820"
|
||||
id="stop4"
|
||||
offset="1" />
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
|
||||
<defs
|
||||
id="defs14"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient862"><stop
|
||||
style="stop-color:#ff6d00;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop858" /><stop
|
||||
style="stop-color:#ff6d00;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop860" /></linearGradient><linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="SVGID_1_"
|
||||
x1="28.7554"
|
||||
x2="82.789497"
|
||||
y1="81.535202"
|
||||
y2="9.2979002"
|
||||
style="fill-opacity:1;fill:url(#linearGradient849)">
|
||||
<stop
|
||||
style="stop-color:#FBB22F;fill-opacity:1;fill:url(#linearGradient849)"
|
||||
id="stop853"
|
||||
offset="0" />
|
||||
<stop
|
||||
style="stop-color:#F58820;fill-opacity:1;fill:url(#linearGradient849)"
|
||||
id="stop855"
|
||||
offset="1" />
|
||||
</linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient847"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop843" /><stop
|
||||
style="stop-color:#000000;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop845" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient847"
|
||||
id="linearGradient849"
|
||||
x1="83.161415"
|
||||
y1="8.9067831"
|
||||
x2="10.194695"
|
||||
y2="92.068085"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient862"
|
||||
id="linearGradient866"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="62.43222"
|
||||
y1="35.349697"
|
||||
x2="9.7942638"
|
||||
y2="93.02298" /></defs>
|
||||
<g
|
||||
id="g9"
|
||||
transform="translate(-1.0593197,1.9067775)"
|
||||
style="fill-opacity:1;fill:url(#linearGradient849)">
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="SVGID_1_-3"
|
||||
x1="28.7554"
|
||||
x2="82.789497"
|
||||
y1="81.535202"
|
||||
y2="9.2979002"
|
||||
style="fill-opacity:1;fill:url(#linearGradient849)">
|
||||
<stop
|
||||
style="stop-color:#FBB22F;fill-opacity:1;fill:url(#linearGradient849)"
|
||||
id="stop2"
|
||||
offset="0" />
|
||||
<stop
|
||||
style="stop-color:#F58820;fill-opacity:1;fill:url(#linearGradient849)"
|
||||
id="stop4"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_1_)"
|
||||
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
|
||||
id="path7" />
|
||||
<path
|
||||
style="fill:url(#linearGradient866);fill-opacity:1"
|
||||
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
|
||||
id="path7" />
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
enable-background="new 0 0 95.334 100"
|
||||
height="100"
|
||||
id="Ebene_1"
|
||||
version="1.1"
|
||||
viewBox="0 0 100 100"
|
||||
width="100"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px"
|
||||
xml:space="preserve"><metadata
|
||||
id="metadata16"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
|
||||
<defs id="defs14" />
|
||||
<g id="g9"
|
||||
transform="translate(-1.0593197,1.9067775)">
|
||||
<linearGradient gradientUnits="userSpaceOnUse"
|
||||
id="SVGID_1_"
|
||||
x1="28.7554"
|
||||
x2="82.789497"
|
||||
y1="81.535202"
|
||||
y2="9.2979002">
|
||||
<stop style="stop-color:#FBB22F"
|
||||
id="stop2"
|
||||
offset="0" />
|
||||
<stop style="stop-color:#F58820"
|
||||
id="stop4"
|
||||
offset="1" />
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_1_)"
|
||||
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
|
||||
id="path7" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="125"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
viewBox="0 0 125 125"
|
||||
width="125"
|
||||
sodipodi:docname="ic_button_done.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.2457711"
|
||||
inkscape:cx="104.26334"
|
||||
inkscape:cy="71.321036"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs6" />
|
||||
<circle
|
||||
style="fill:none;fill-opacity:1;stroke:#ff6d00;stroke-width:5.03185;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="62.5"
|
||||
cy="62.5"
|
||||
id="path12"
|
||||
r="59.984074" />
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:title></dc:title>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<circle
|
||||
style="fill:#ff6d00;fill-opacity:1;stroke:none;stroke-width:6.91728;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="62.5"
|
||||
cy="62.5"
|
||||
id="path835"
|
||||
r="50" />
|
||||
<path
|
||||
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 38.620483,63.865643 52.899055,78.144215 86.379516,44.663754"
|
||||
id="path1000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="125"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
viewBox="0 0 125 125"
|
||||
width="125"
|
||||
sodipodi:docname="ic_button_resume.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview9"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.176"
|
||||
inkscape:cx="85.245821"
|
||||
inkscape:cy="70.9649"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs6" />
|
||||
<circle
|
||||
style="fill:none;fill-opacity:1;stroke:#ff6d00;stroke-width:5.03185;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="62.5"
|
||||
cy="62.5"
|
||||
id="path12"
|
||||
r="59.984074" />
|
||||
<circle
|
||||
style="fill:#ff6d00;fill-opacity:1;stroke:none;stroke-width:6.91728;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="62.5"
|
||||
cy="62.5"
|
||||
id="path835"
|
||||
r="50" />
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:title></dc:title>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.67087;stroke-linecap:round;stroke-opacity:1"
|
||||
id="path837"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="61.453945"
|
||||
sodipodi:cy="62.5"
|
||||
sodipodi:r1="26.146271"
|
||||
sodipodi:r2="13.073136"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="M 87.600216,62.5 67.990513,73.821668 48.38081,85.143335 l -10e-7,-22.643336 10e-7,-22.643334 19.609704,11.321667 z"
|
||||
inkscape:transform-center-x="-6.5365678" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -7,4 +7,4 @@ Funktionen & Features:
|
||||
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
|
||||
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
|
||||
* exportiert deine Daten als KML oder GPX
|
||||
* funktioniert ganz ohne Internet
|
||||
* funktioniert ganz ohne Internet
|
||||
|
||||
@@ -1 +1 @@
|
||||
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
|
||||
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
|
||||
|
||||
@@ -4,3 +4,4 @@ Changes:
|
||||
- Configurable filename for export/sharing (default: {starttime}_{name}.{suffix})
|
||||
- Vertical GPS accuracy is now stored and can be exported/imported
|
||||
(API level >= 26).
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
v4.0.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- UI refactoring with a Bottom App Bar.
|
||||
- Public API: implemented STOP properly.
|
||||
- Cleanup: use explicit preference identifiers for TrackFileFormat.
|
||||
- Improvements on BLE handler.
|
||||
- Add exception handler and show error activity so users can send reports about bugs.
|
||||
- Android 13: launcher icon, provide a themed icon.
|
||||
- Change track title capitalization to sentences.
|
||||
- Translations updated.
|
||||
|
||||
Bugfix:
|
||||
- Sharing track added file suffix twice.
|
||||
- Unit system not loaded correctly.
|
||||
- File format template was not loaded (NPE).
|
||||
- Public API: respect post-workout export.
|
||||
|
||||
Developer:
|
||||
- Move to androix.constraintlayout.
|
||||
- UI tests slightly more robust.
|
||||
- Refactor permission request.
|
||||
- Upgrade to gradlew 7.4.2
|
||||
- Bump material from 1.5.0 to 1.6.0
|
||||
@@ -0,0 +1,13 @@
|
||||
v4.0.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Voice announcements are now configurable
|
||||
- Fullscreen mode now also hides Android's navigation bar
|
||||
- Connection to BLE sensors should now be faster
|
||||
- Allow to reset settings while recording
|
||||
- Allow to change import/export settings while recording
|
||||
- KML/KMZ: do not use icons to be loaded from Google
|
||||
|
||||
Bugfix:
|
||||
- TrackStoppedActivity now works on smaller screens
|
||||
- Requests permissions on track list
|
||||
@@ -0,0 +1,10 @@
|
||||
v4.0.2: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Add nautical miles
|
||||
- Settings can be reset while recording
|
||||
|
||||
Bugfix:
|
||||
- Voice announcement rounding
|
||||
- Chart: elevation was weirdly smoothed
|
||||
- Export was not checking if file exists
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.0.3: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Max. speed as reported by sensor (GPS or BLE)
|
||||
|
||||
Bugfix:
|
||||
- Prevent crash if OSM Dashboard is not installed
|
||||
- GPX track export: do not export empty track segments
|
||||
@@ -0,0 +1,13 @@
|
||||
v4.0.4: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Store BLE sensor data even without distance (every 10s)
|
||||
- Public API: allows setting name, category, and description
|
||||
|
||||
Bugfix:
|
||||
- Crash when taking a picture and rotating the phone
|
||||
-
|
||||
|
||||
Developer:
|
||||
- Target SDK 33
|
||||
- Barometer uses interval of 5s
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.0.5: OpenTracks
|
||||
|
||||
Changes:
|
||||
- PlayStore relase get's another logo
|
||||
|
||||
Bugfix:
|
||||
- Interval list shows "-" if gain/loss is not present
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.0.6: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- EGM2008 offset computed wrong for negative longitudes
|
||||
- PublicAPI crashed if OpenTracks was in background (API26+)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.0.7: OpenTracks
|
||||
|
||||
Changes:
|
||||
- GPS signal lost timeout: 30s
|
||||
- Android13: request permission to show notifications
|
||||
- Faster deletion of tracks (vacuum database only if needed)
|
||||
|
||||
Bugfix:
|
||||
- PublicAPI stop command resulted in crash (introduced in v4.0.6)
|
||||
- Permissions: request all permissions during on startup
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.0.8: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Altitude always shown in m/ft
|
||||
- Show track timezone if different than device timezone
|
||||
- PublicAPI: support setting the track icon explicitly
|
||||
|
||||
Developer:
|
||||
- Introduced DistanceFormatter
|
||||
- Upgrade to AGP 7.3/desugar for Duration.class
|
||||
- Android12+: do splashscreen via Androidx
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.1.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Export KML/KMZ version 2.3 only (was 2.2)
|
||||
- For crank-based powermeters: fetch cadence
|
||||
|
||||
Bugfix:
|
||||
- Workaround for crash on "import all"
|
||||
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 812 KiB |
|
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 630 KiB After Width: | Height: | Size: 695 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 509 KiB |
|
Before Width: | Height: | Size: 798 KiB After Width: | Height: | Size: 872 KiB |
@@ -0,0 +1,11 @@
|
||||
v3.20.2 : OpenTracks
|
||||
|
||||
Changements :
|
||||
- Le gain et la perte d'altitude sont des entiers au lieu de floats
|
||||
- Demander la permission avant d'utiliser Bluetooth par un message Toast
|
||||
|
||||
Correction d'un bug :
|
||||
- Définir FLAG_IMMUTABLE pour PendingIntents pour éviter un crash sur Android 12
|
||||
- Le capteur de cadence renvoie 0 au lieu de None lorsque l'utilisateur ne pédale pas.
|
||||
- Le capteur de vitesse renvoie 0 au lieu de la dernière vitesse connue quand c'est arrêté
|
||||
- Avec les capteurs, le temps de déplacement s'arrête lorsqu'il n'est pas en mouvement
|
||||
@@ -7,5 +7,11 @@ Fonctionnalités :
|
||||
* Dénivelé positif et négatif: via capteur de pression barométrique
|
||||
* Synthèse vocale
|
||||
* Prise en charge des capteurs Bluetooth LE : rythme cardiaque, vitesse et distance (vélo), cadence de pédalage (vélo), et capteur de puissance (vélo)
|
||||
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
|
||||
* Export : export des traces au format KMZ (photos incluses), KML ou GPX
|
||||
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
|
||||
* Thème sombre ou claire, avec le respect des paramètres du système
|
||||
* Pas de publicités
|
||||
|
||||
Logiciel Open Source
|
||||
Vous pouvez utiliser, étudier, changer et partager le code source.
|
||||
Licence Apache 2.0
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
v3.18.6: OpenTracks
|
||||
|
||||
Wijzigingen:
|
||||
- "Toon op kaart"-functionaliteit via KML in plaats van KMZ
|
||||
- Spraakaankondigingen bevatten geen app naam
|
||||
- Tijdens opname: hoogtewinst/-verlies wordt elke seconde bijgewerkt
|
||||
- Winkel beschrijvingen vertaald
|
||||
|
||||
Bufixes:
|
||||
- Ongeldige vertalingen van stemboodschappen opgelost
|
||||
|
||||
Ontwikkelaar:
|
||||
- Android12-toestemmingen (BLUETOOTH_SCAN)
|
||||
- Android12-toestemmingen (ACCESS_COARSE_LOCATION)
|
||||
- Assertiviteit voor TrackPoints (testen)
|
||||
- Ongebruikte vertalingen verwijderd
|
||||
@@ -0,0 +1,16 @@
|
||||
v3.19.0: OpenTracks
|
||||
|
||||
Wijzigingen:
|
||||
- Ondersteuning voor Running Speed en Cadence (RCS) sensoren
|
||||
- Bij het delen van een track wordt een afbeelding gemaakt
|
||||
- Niet trillen bij starten van opnemen
|
||||
- Bij opnemen: UI toont recentst ontvangen hoogte, snelheid en richting (ook al is deze niet opgeslagen)
|
||||
|
||||
Bugfix:
|
||||
- Berekening van totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren)
|
||||
- (opnieuw) Gebruik aangepaste overzichten indien aanwezig
|
||||
|
||||
Ontwikkelaar:
|
||||
- Upgrade naar SDK31 en AGP7.0.0 (vereist JDK11)
|
||||
- TrackRecordingService: sensorfusie (TrackPointCreator) en TrackPoint-opslag (TrackRecordingManager) geëxtraheerd
|
||||
- ExportImportTest is nu deterministisch/constant
|
||||
@@ -0,0 +1,22 @@
|
||||
Gizliliğinize saygı duyan bir spor takipçisi.
|
||||
|
||||
Spor ve açık hava etkinlikleri mutluluk, beceri ve güven verir.
|
||||
|
||||
Antrenmanınızı takip ederek sağlığınıza değer verin.
|
||||
Koşarken veya yürürken kaydeder ve size bisiklet sürmek için daha büyük ekranlı bir bisiklet bilgisayarı verir.
|
||||
Yol boyunca ilginç yerleri resimlerle işaretleyin.
|
||||
Analiz için kaydedilen istatistikleri çok ayrıntılı tutun.
|
||||
Yalnızca başkalarının sahip olmasını istediğiniz verileri paylaşın.
|
||||
|
||||
* Sesli duyurular.
|
||||
* Bluetooth LE sensörlerini destekler: kalp atış hızı, hız ve mesafe (bisiklet), tempo (bisiklet) ve güç ölçer (bisiklet).
|
||||
* İrtifa kazancı ve kaybı: barometrik sensör aracılığıyla.
|
||||
* EGM2008'de gösterilen rakım (ortalama deniz seviyesinin üzerinde); WGS84 olarak dışa aktarılır.
|
||||
* Verileri KMZ (fotoğraflar dahil), KML veya GPX olarak parça olarak dışa aktarın.
|
||||
* İnternet erişimi veya ekstra izin yok.
|
||||
* Sistem ayarlarına uygun olarak koyu ve açık tema.
|
||||
* Reklamsız.
|
||||
|
||||
Libre yazılımı / Özgür Yazılım / Açık Kaynak
|
||||
yani kaynak kodunu kullanabilir, inceleyebilir, değiştirebilir ve paylaşabilirsiniz.
|
||||
Lisanslı Apache 2.0
|
||||
@@ -0,0 +1 @@
|
||||
Gizliliğinize saygı duyan bir spor takipçisi.
|
||||
@@ -1,11 +1,22 @@
|
||||
OpenTracks是一个完全尊重你的隐私的运动跟踪应用程序。
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
|
||||
健康幸福的生活总少不了各种运动和户外活动。
|
||||
|
||||
使用本应用,您可以保持对您的训练数据的有效追踪,为您的健康助力。
|
||||
本应用可以在您跑步或散步时记录数据,也可以在您骑车时作为一个大屏幕自行车电脑。
|
||||
您可以为路上的有趣位置创建标记,还可以用图片作为标记。
|
||||
您的数据会被详尽地记录下来,供您查阅分析。
|
||||
您的数据只会分享给您想要分享给的人。
|
||||
|
||||
特点。
|
||||
* 运动追踪:追踪你的运动和户外活动
|
||||
* 照片和标记:在跟踪时标记有趣的地点
|
||||
* 海拔增减:通过气压传感器
|
||||
* 高度显示为EGM2008(平均海平面以上);导出为WGS84
|
||||
* 语音播报
|
||||
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、节奏(骑车)和功率计(骑车)。
|
||||
* 导出数据:以KMZ(包括照片)、KML或GPX形式导出轨迹
|
||||
* 没有互联网接入。不使用互联网
|
||||
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、踏频(骑车)和功率计(骑车)。
|
||||
* 海拔增减:通过气压传感器。
|
||||
* EGM2008 海拔显示(平均海平面以上);导出为 WGS84。
|
||||
* 导出路线数据,支持 KMZ(包括照片)、KML 和 GPX 格式。
|
||||
* 不需要联网,不需要额外权限。
|
||||
* 深色和浅色主题,跟随系统设置。
|
||||
* 没有广告。
|
||||
|
||||
本应用是开源软件
|
||||
意味着您可以使用、学习、修改和分发本应用的源代码。
|
||||
本应用使用 Apache 2.0 协议发布。
|
||||
|
||||
@@ -1 +1 @@
|
||||
一个完全尊重你隐私的运动追踪器。
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 862 KiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 1001 KiB After Width: | Height: | Size: 1012 KiB |
@@ -3,48 +3,25 @@ package de.dennisguse.opentracks;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.pressBack;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.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.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.withListSize;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.test.espresso.IdlingRegistry;
|
||||
import androidx.test.espresso.IdlingResource;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.assertion.ViewAssertions;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoDeleteTrackTest {
|
||||
@@ -55,150 +32,33 @@ public class EspressoDeleteTrackTest {
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
private MyIdlingResource idlingResource;
|
||||
|
||||
@Before
|
||||
public void registerIntentServiceIdlingResource() {
|
||||
Instrumentation instrumentation
|
||||
= InstrumentationRegistry.getInstrumentation();
|
||||
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
|
||||
IdlingRegistry.getInstance().register(idlingResource);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unregisterIntentServiceIdlingResource() {
|
||||
IdlingRegistry.getInstance().unregister(idlingResource);
|
||||
}
|
||||
|
||||
@Ignore("Test fails permanently")
|
||||
@Deprecated
|
||||
@Test
|
||||
public void espressoDeleteTrackTest() {
|
||||
int countBefore;
|
||||
// TrackListActivity: start recording
|
||||
onView(withId(R.id.track_list_fab_action)).perform(click());
|
||||
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
// TrackRecordingActivity
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
// wait; stay recording
|
||||
.perform(waitFor(5000))
|
||||
// stop;
|
||||
.perform(longClick());
|
||||
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(5000));
|
||||
|
||||
// stop;
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
}
|
||||
|
||||
// back
|
||||
pressBack();
|
||||
|
||||
// get number of items before deleting a track
|
||||
countBefore = numberOfItemsListView();
|
||||
// TrackStoppedActivity
|
||||
onView(withId(R.id.finish_button)).perform(click());
|
||||
|
||||
// select track
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
|
||||
|
||||
// open menu and delete selected track
|
||||
ViewInteraction overflowMenuButton = onView(
|
||||
allOf(withContentDescription("More options"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.toolbar),
|
||||
2),
|
||||
2),
|
||||
isDisplayed()));
|
||||
overflowMenuButton.perform(click());
|
||||
onView(allOf(withContentDescription("More options"), isDisplayed()))
|
||||
.perform(click());
|
||||
|
||||
ViewInteraction appCompatTextView = onView(
|
||||
allOf(withId(R.id.title), withText("Delete"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.content),
|
||||
0),
|
||||
0),
|
||||
isDisplayed()));
|
||||
appCompatTextView.perform(click());
|
||||
onView(withText("Delete")).perform(click());
|
||||
|
||||
ViewInteraction appCompatButton = onView(
|
||||
allOf(withId(android.R.id.button1), withText("Yes"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(R.id.buttonPanel),
|
||||
0),
|
||||
3)));
|
||||
appCompatButton.perform(scrollTo(), click());
|
||||
onView(withText("OK")).perform(click());
|
||||
|
||||
// check number of items after delete
|
||||
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
|
||||
}
|
||||
|
||||
|
||||
private int numberOfItemsListView() {
|
||||
final int[] counts = new int[1];
|
||||
onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher<View>() {
|
||||
@Override
|
||||
public boolean matchesSafely(View view) {
|
||||
ListView listView = (ListView) view;
|
||||
|
||||
counts[0] = listView.getCount();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
}
|
||||
}));
|
||||
|
||||
return counts[0];
|
||||
}
|
||||
|
||||
|
||||
private static class MyIdlingResource implements IdlingResource {
|
||||
|
||||
private final Context context;
|
||||
private ResourceCallback resourceCallback;
|
||||
|
||||
public MyIdlingResource(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyIdlingResource.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
|
||||
this.resourceCallback = resourceCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdleNow() {
|
||||
boolean idle = !isDeleteServiceRunning();
|
||||
if (idle && resourceCallback != null) {
|
||||
resourceCallback.onTransitionToIdle();
|
||||
}
|
||||
return idle;
|
||||
}
|
||||
|
||||
private boolean isDeleteServiceRunning() {
|
||||
ActivityManager manager =
|
||||
(ActivityManager) context.getSystemService(
|
||||
Context.ACTIVITY_SERVICE);
|
||||
for (ActivityManager.RunningServiceInfo info :
|
||||
manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (TrackDeleteService.class.getName().equals(
|
||||
info.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// tracklist is empty now
|
||||
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,14 @@ import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
@@ -25,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -50,16 +45,15 @@ public class EspressoEditTrackRecordingTest {
|
||||
public void espressoEditTrackRecordingTest() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
fabRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(15000));
|
||||
trackControllerStopButton.perform(waitFor(4000));
|
||||
|
||||
// open menu
|
||||
openContextualActionModeOverflowMenu();
|
||||
@@ -68,58 +62,24 @@ public class EspressoEditTrackRecordingTest {
|
||||
onView(withText(R.string.menu_edit)).perform(click());
|
||||
|
||||
// change name for "New Name"
|
||||
ViewInteraction textInputEditText = onView(
|
||||
allOf(withId(R.id.track_edit_name),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
|
||||
0),
|
||||
1)));
|
||||
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name));
|
||||
textInputEditText.perform(scrollTo(), replaceText("New Name"));
|
||||
|
||||
ViewInteraction textInputEditText2 = onView(
|
||||
allOf(withId(R.id.track_edit_name), withText("New Name"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
|
||||
0),
|
||||
1),
|
||||
isDisplayed()));
|
||||
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()));
|
||||
textInputEditText2.perform(closeSoftKeyboard());
|
||||
|
||||
// save edition
|
||||
ViewInteraction appCompatButton = onView(
|
||||
allOf(withId(R.id.track_edit_save),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withClassName(is("android.widget.LinearLayout")),
|
||||
3),
|
||||
1),
|
||||
isDisplayed()));
|
||||
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save));
|
||||
appCompatButton.perform(click());
|
||||
|
||||
//pause
|
||||
trackControllerRecordButton.perform(veryLongTouch(1600));
|
||||
|
||||
// wait; stay paused
|
||||
trackControllerRecordButton.perform(waitFor(1000));
|
||||
|
||||
// resume
|
||||
trackControllerRecordButton.perform(click());
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(1000));
|
||||
|
||||
// stop;
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
trackControllerStopButton.perform(longClick());
|
||||
|
||||
// check track's name is "New Name"
|
||||
ViewInteraction textView = onView(
|
||||
allOf(withId(R.id.stats_name_value),
|
||||
withParent(withParent(IsInstanceOf.instanceOf(android.widget.ScrollView.class))),
|
||||
isDisplayed()));
|
||||
textView.check(matches(withText("New Name")));
|
||||
// it's on track stopped activity and there are two buttons
|
||||
ViewInteraction resumeButton = onView(withId(R.id.resume_button));
|
||||
resumeButton.check(matches(isDisplayed()));
|
||||
ViewInteraction finishButton = onView(withId(R.id.finish_button));
|
||||
finishButton.check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,26 +3,22 @@ package de.dennisguse.opentracks;
|
||||
import static androidx.test.espresso.Espresso.onData;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
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 org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.action.MotionEvents;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -39,34 +35,30 @@ public class EspressoUITest {
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void record_pause_resume_stop() {
|
||||
public void record_stop_resume_stop_finish() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
// TrackRecordingActivity: wait to record some time and then stop
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
.perform(waitFor(5000))
|
||||
.perform(longClick());
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(5000));
|
||||
// TrackStoppedActivity: resume
|
||||
onView(allOf(withId(R.id.resume_button), isClickable()))
|
||||
.perform(click());
|
||||
|
||||
//pause
|
||||
trackControllerRecordButton.perform(veryLongTouch(1600));
|
||||
// TrackRecordingActivity: wait and then stop
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
.perform(waitFor(5000))
|
||||
.perform(longClick());
|
||||
|
||||
// wait; stay paused
|
||||
trackControllerRecordButton.perform(waitFor(1000));
|
||||
|
||||
// resume
|
||||
trackControllerRecordButton.perform(click());
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(1000));
|
||||
|
||||
// stop;
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
// TrackStoppedActivity
|
||||
onView(withId(R.id.finish_button))
|
||||
.perform(click());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +67,13 @@ public class EspressoUITest {
|
||||
public void record_move_through_tabs() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(1));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
@@ -96,7 +88,7 @@ public class EspressoUITest {
|
||||
tabLayout.perform(waitFor(1000));
|
||||
|
||||
// stop
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
trackControllerStopButton.perform(longClick());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,36 +96,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(veryLongTouch(2000));
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(longClick());
|
||||
}
|
||||
|
||||
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Perform long touch.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
// Get view absolute position
|
||||
int[] location = new int[2];
|
||||
view.getLocationOnScreen(location);
|
||||
|
||||
// Offset coordinates by view position
|
||||
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
|
||||
|
||||
// Send down event, pause, and send up
|
||||
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
MotionEvents.sendUp(uiController, down, coordinates);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ChartPointTest {
|
||||
statistics.setTotalTime(Duration.ofSeconds(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), false, UnitSystem.IMPERIAL);
|
||||
|
||||
// then
|
||||
assertEquals(1000000, (long) point.getTimeOrDistance());
|
||||
@@ -40,7 +40,7 @@ public class ChartPointTest {
|
||||
statistics.setTotalDistance(Distance.of(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), true, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(1, (long) point.getTimeOrDistance());
|
||||
@@ -50,9 +50,11 @@ public class ChartPointTest {
|
||||
public void create_get_altitude_speed_and_pace() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||
.setAltitude(Altitude.EGM2008.of(50));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(50, point.getAltitude(), 0.01);
|
||||
@@ -64,9 +66,10 @@ public class ChartPointTest {
|
||||
public void create_sensorNotAvailable() {
|
||||
// given
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||
.setAltitude(Altitude.EGM2008.of(50));
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertNull(point.getHeartRate());
|
||||
@@ -77,15 +80,16 @@ public class ChartPointTest {
|
||||
@Test
|
||||
public void create_sensorAvailable() {
|
||||
// given
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
|
||||
trackPoint.setHeartRate(100f);
|
||||
trackPoint.setCadence(101f);
|
||||
trackPoint.setPower(102f);
|
||||
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint()
|
||||
.setAltitude(Altitude.EGM2008.of(50))
|
||||
.setHeartRate(100f)
|
||||
.setCadence(101f)
|
||||
.setPower(102f);
|
||||
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(100.0, point.getHeartRate(), 0.01);
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
*/
|
||||
package de.dennisguse.opentracks.chart;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@@ -25,10 +29,6 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests {@link ChartValueSeries}.
|
||||
*
|
||||
@@ -47,6 +47,7 @@ public class ChartValueSeriesTest {
|
||||
new int[]{1, 100, 1000},
|
||||
R.string.description_altitude_metric,
|
||||
R.string.description_altitude_imperial,
|
||||
R.string.description_altitude_imperial,
|
||||
R.color.chart_altitude_fill,
|
||||
R.color.chart_altitude_border,
|
||||
15,
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.util.Pair;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -24,22 +23,17 @@ public class ShareContentProviderTest {
|
||||
|
||||
@Test
|
||||
public void testCreateAndParseURI_valid() {
|
||||
Set<Track.Id> trackIds = new HashSet<>();
|
||||
trackIds.add(new Track.Id(1));
|
||||
trackIds.add(new Track.Id(3));
|
||||
trackIds.add(new Track.Id(5));
|
||||
Set<Track.Id> trackIds = Set.of(new Track.Id(1), new Track.Id(3), new Track.Id(5));
|
||||
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "TrackName", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
|
||||
assertEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateURIescapeFilename() {
|
||||
Set<Track.Id> trackIds = new HashSet<>();
|
||||
trackIds.add(new Track.Id(1));
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(Set.of(new Track.Id(1)), "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
|
||||
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_with_trackdetail_and_sensordata/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
|
||||
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import junit.framework.TestCase;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -105,7 +106,7 @@ public class TrackSelectionTest extends TestCase {
|
||||
long oneDay = 24 * 60 * 60 * 1000;
|
||||
TrackSelection filterWrong1 = new TrackSelection().addDateRange(instant, null);
|
||||
TrackSelection filterWrong2 = new TrackSelection().addDateRange(null, instant);
|
||||
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plusMillis(oneDay));
|
||||
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plus(Duration.ofDays(1)));
|
||||
|
||||
// when
|
||||
SelectionData selectionWrong1 = filterWrong1.buildSelection();
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
//TODO Parametrized tests
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DistanceFormatterTest {
|
||||
|
||||
@@ -42,4 +43,17 @@ public class DistanceFormatterTest {
|
||||
// A small number in imperial
|
||||
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatDistance_nautical() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.build(context);
|
||||
|
||||
// A large number in nautical
|
||||
assertEquals("2.70 NM", formatter.formatDistance(Distance.of(5000)));
|
||||
// A small number in nautical
|
||||
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
//TODO Use make parametrized tests?
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SpeedFormatterTest {
|
||||
|
||||
@@ -39,12 +40,22 @@ public class SpeedFormatterTest {
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
|
||||
|
||||
assertEquals("8:02", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_nautical() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
assertEquals("9:14", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
assertEquals("min/NM", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_metric() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
@@ -69,6 +80,18 @@ public class SpeedFormatterTest {
|
||||
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_nautical() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
|
||||
assertEquals("6.5", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
assertEquals("kn", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
|
||||
@@ -28,10 +28,11 @@ public class TrackFilenameGeneratorTest {
|
||||
@Parameterized.Parameters
|
||||
public static Collection<String[]> data() {
|
||||
return Arrays.asList(new String[][]{
|
||||
{"{uuid}_{name}", "0000fee0_Best Track.gpx" },
|
||||
{"{name}_{uuid}", "Best Track_0000fee0.gpx" },
|
||||
{"{time}_{name}", "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||
{"{uuid}_{name}", "0000fee0_Best Track.gpx"},
|
||||
{"{name}_{uuid}", "Best Track_0000fee0.gpx"},
|
||||
{"{time}_{name}", "02_02_02_Best Track.gpx"},
|
||||
{"{date}_{name}", "2020-02-02_Best Track.gpx"},
|
||||
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02_02_02_02_Best Track.gpx"},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@ public class KmlTrackExporterTest {
|
||||
@Test
|
||||
public void writeCloseSegment_only_write_sensordata_if_present() {
|
||||
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<coord/>\n" +
|
||||
"<when>1970-01-01T01:00:00+01:00</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<coord/>\n" +
|
||||
"<ExtendedData>\n" +
|
||||
"<SchemaData schemaUrl=\"#schema\">\n" +
|
||||
"</SchemaData>\n" +
|
||||
"</ExtendedData>\n" +
|
||||
"</gx:Track>\n";
|
||||
"</Track>\n";
|
||||
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
|
||||
|
||||
@@ -61,7 +61,8 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -155,27 +156,32 @@ public class ExportImportTest {
|
||||
mockAltitudeChange(trackPointCreator, 1);
|
||||
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:05Z");
|
||||
trackPointCreator.setClock("2020-02-02T02:02:14Z");
|
||||
mockBLESensorData(trackPointCreator, 15f, null, 66f, 3f, 50f);
|
||||
trackPointCreator.setClock("2020-02-02T02:02:15Z");
|
||||
mockBLESensorData(trackPointCreator, null, null, 66f, 3f, 50f);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:16Z");
|
||||
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:06Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
|
||||
service.pauseCurrentTrack();
|
||||
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||
service.endCurrentTrack();
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:20Z");
|
||||
service.resumeCurrentTrack();
|
||||
trackPointCreator.setClock("2020-02-02T02:03:20Z");
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:24Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
|
||||
trackPointCreator.setClock("2020-02-02T02:03:24Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||
service.endCurrentTrack();
|
||||
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
@@ -187,7 +193,7 @@ public class ExportImportTest {
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||
markers = contentProviderUtils.getMarkers(trackId);
|
||||
assertEquals(10, trackPoints.size());
|
||||
assertEquals(12, trackPoints.size());
|
||||
assertEquals(2, markers.size());
|
||||
}
|
||||
|
||||
@@ -231,29 +237,29 @@ 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:02:24Z"), importedTrackStatistics.getStopTime());
|
||||
assertEquals(Instant.parse("2020-02-02T02:03:24Z"), importedTrackStatistics.getStopTime());
|
||||
|
||||
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
|
||||
|
||||
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(7), importedTrackStatistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getTotalTime());
|
||||
|
||||
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
|
||||
|
||||
// Distance
|
||||
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
|
||||
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||
|
||||
// Speed
|
||||
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
|
||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
|
||||
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
|
||||
assertEquals(17.59, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||
assertEquals(6.58, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||
|
||||
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
|
||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
|
||||
// Altitude
|
||||
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
|
||||
@@ -345,19 +351,19 @@ public class ExportImportTest {
|
||||
.setAltitudeLoss(1f)
|
||||
.setAltitudeGain(1f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:17Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(14.001)
|
||||
.setAltitude(10)
|
||||
.setSpeed(Speed.of(5))
|
||||
.setAltitudeLoss(1f)
|
||||
.setAltitudeGain(1f)
|
||||
.setSensorDistance(Distance.of(12))
|
||||
.setSensorDistance(Distance.of(14))
|
||||
.setHeartRate(66f)
|
||||
.setPower(50f)
|
||||
.setCadence(3f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:21Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(14.002)
|
||||
.setAltitude(10)
|
||||
@@ -365,7 +371,7 @@ public class ExportImportTest {
|
||||
.setAltitudeGain(0f)
|
||||
.setSpeed(Speed.of(15))
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:22Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(16)
|
||||
.setAltitude(10)
|
||||
@@ -373,7 +379,7 @@ public class ExportImportTest {
|
||||
.setAltitudeGain(0f)
|
||||
.setSpeed(Speed.of(15))
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:23Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(16.001)
|
||||
.setAltitude(10)
|
||||
@@ -390,18 +396,18 @@ public class ExportImportTest {
|
||||
// Time
|
||||
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
|
||||
assertEquals(Instant.parse("2020-02-02T02:03:23Z"), importedTrackStatistics.getStopTime());
|
||||
|
||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
|
||||
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getTotalTime());
|
||||
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getMovingTime());
|
||||
|
||||
// Distance
|
||||
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
|
||||
|
||||
// Speed
|
||||
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(41.05, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||
assertEquals(41.05, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
|
||||
assertEquals(8.34, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(8.34, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
|
||||
assertEquals(8.34, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
|
||||
|
||||
// Altitude
|
||||
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
|
||||
@@ -458,7 +464,7 @@ public class ExportImportTest {
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// then
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.csv_export);
|
||||
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
@@ -492,23 +498,23 @@ public class ExportImportTest {
|
||||
}
|
||||
|
||||
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator) {
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator) {
|
||||
@Override
|
||||
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
|
||||
SensorDataSet sensorDataSet = new SensorDataSet();
|
||||
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
|
||||
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
|
||||
|
||||
SensorDataCycling.CyclingCadence cyclingCadence = Mockito.mock(SensorDataCycling.CyclingCadence.class);
|
||||
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) {
|
||||
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
|
||||
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
|
||||
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
|
||||
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
|
||||
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
|
||||
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
|
||||
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
|
||||
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
|
||||
sensorDataSet.set(distanceSpeed);
|
||||
|
||||
@@ -15,11 +15,16 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -27,13 +32,15 @@ import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class GPXImportTest {
|
||||
public class GPXTrackImporterTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -62,7 +69,7 @@ public class GPXImportTest {
|
||||
public void gpx_with_pause_resume() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_gpx_pause_resume);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -116,7 +123,7 @@ public class GPXImportTest {
|
||||
public void gpx_without_speed() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_without_speed);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_without_speed);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -164,7 +171,7 @@ public class GPXImportTest {
|
||||
public void gpx_speed_no_namespace() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_with_speed_no_namespace);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_with_speed_no_namespace);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -181,7 +188,7 @@ public class GPXImportTest {
|
||||
|
||||
// 3. trackstatistics
|
||||
TrackStatistics trackStatistics = importedTrack.getTrackStatistics();
|
||||
assertEquals(4, trackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(4.0, trackStatistics.getMaxSpeed().toMPS(), 0.01);
|
||||
assertEquals(Duration.ofSeconds(101), trackStatistics.getMovingTime());
|
||||
|
||||
// 4. trackpoints
|
||||
@@ -208,4 +215,31 @@ public class GPXImportTest {
|
||||
.setSpeed(Speed.of(3))
|
||||
), importedTrackPoints);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void importExportTest_timezone() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
|
||||
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||
trackExporter.writeTrack(importedTrack, outputStream);
|
||||
|
||||
// then
|
||||
String expected = new BufferedReader(
|
||||
new InputStreamReader(inputStreamExpected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n")) + "\n";
|
||||
|
||||
assertEquals(expected, outputStream.toString()); //TODO inputStream.readAllBytes() ?
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class KMLImportTest {
|
||||
public class KMLTrackImporterTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -60,7 +60,7 @@ public class KMLImportTest {
|
||||
public void kml22_order_location_and_when() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_order_location_and_when);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -109,7 +109,7 @@ public class KMLImportTest {
|
||||
public void kml22_time_decreases() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_time_decreases);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -149,7 +149,7 @@ public class KMLImportTest {
|
||||
public void kml22_with_statistics_marker() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_kml22_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -223,7 +223,7 @@ public class KMLImportTest {
|
||||
public void kml_without_locations() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_without_locations);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
@@ -234,7 +234,7 @@ public class KMLImportTest {
|
||||
public void kml_when_locations_different() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_when_locations_different);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
@@ -10,7 +10,8 @@ import org.junit.Test;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
public class BluetoothUtilsTest {
|
||||
@@ -18,7 +19,7 @@ public class BluetoothUtilsTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
@@ -31,7 +32,7 @@ public class BluetoothUtilsTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
@@ -43,11 +44,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertNull(sensor.getDistanceSpeed());
|
||||
@@ -56,11 +57,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
@@ -69,11 +70,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
@@ -82,19 +83,34 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
|
||||
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, power_w);
|
||||
assertEquals(40, powerCadence.getPower().getValue().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
|
||||
|
||||
// when
|
||||
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(0, powerCadence.getPower().getValue().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.getCadence().getCrankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.getCadence().getCrankRevolutionsTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseRunningSpeedAndCadence_with_distance() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
|
||||
|
||||
// when
|
||||
|
||||
@@ -21,8 +21,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_1() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -34,8 +34,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_2() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 8016);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -47,8 +47,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_sameCount() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -61,8 +61,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_sameTime() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 1024);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_rollOverTime() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 0);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -89,8 +89,8 @@ public class SensorDataCyclingTest {
|
||||
@Deprecated
|
||||
public void compute_cadence_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_overflow() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -115,8 +115,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_speed() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2150));
|
||||
@@ -131,8 +131,8 @@ public class SensorDataCyclingTest {
|
||||
@Deprecated
|
||||
public void compute_speed_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
@@ -145,8 +145,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_speed_overflow() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
@@ -158,8 +158,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void equals_speed_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress");
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress");
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
@@ -172,8 +172,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void equals_cadence_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress");
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress");
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
@@ -31,12 +31,14 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
@@ -159,7 +161,7 @@ public class TrackRecordingServiceTestRecording {
|
||||
// when
|
||||
String pauseTime = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(pauseTime);
|
||||
service.pauseCurrentTrack();
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
assertEquals(new TrackStatistics(startTime, pauseTime, 0, 1, 0, 0, 0f, 0f)
|
||||
@@ -205,7 +207,7 @@ public class TrackRecordingServiceTestRecording {
|
||||
|
||||
String pauseTime = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(pauseTime);
|
||||
service.pauseCurrentTrack();
|
||||
service.endCurrentTrack();
|
||||
|
||||
// when
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
@@ -257,6 +259,47 @@ public class TrackRecordingServiceTestRecording {
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_blesensor_only_no_distance() {
|
||||
// given
|
||||
String startTime = "2020-02-02T02:02:02Z";
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.stopGPS();
|
||||
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
|
||||
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
|
||||
|
||||
// when
|
||||
String sensor1 = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(sensor1);
|
||||
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
|
||||
|
||||
String sensor3 = "2020-02-02T02:02:13Z";
|
||||
trackPointCreator.setClock(sensor3);
|
||||
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
|
||||
|
||||
String stopTime = "2020-02-02T02:02:15Z";
|
||||
trackPointCreator.setClock(stopTime);
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
new TrackPointAssert().assertEquals(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
|
||||
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
.setHeartRate(HeartRate.of(7)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
.setHeartRate(HeartRate.of(7))
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_gpsOnly_recordingDistance_above() {
|
||||
|
||||
@@ -143,7 +143,7 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_startPauseStopResume_no_data() throws InterruptedException {
|
||||
public void recording_startStopResume_no_data() throws InterruptedException {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
@@ -153,31 +153,19 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
|
||||
service.endCurrentTrack();
|
||||
|
||||
// when
|
||||
service.pauseCurrentTrack();
|
||||
service.resumeTrack(trackId);
|
||||
Thread.sleep(1000);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertTrue(service.isPaused());
|
||||
assertEquals(new RecordingStatus(trackId, true), service.getRecordingStatusObservable().getValue());
|
||||
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); //TODO The GPS is kept in pause, right? Might be a bug.
|
||||
|
||||
|
||||
// when
|
||||
service.resumeCurrentTrack();
|
||||
Thread.sleep(1000);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertFalse(service.isPaused());
|
||||
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
|
||||
@@ -188,7 +176,6 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertFalse(service.isPaused());
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
@@ -200,8 +187,7 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertFalse(service.isPaused());
|
||||
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
|
||||
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
@@ -209,29 +195,31 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void invalidStateChanges() throws InterruptedException {
|
||||
public void cannotResume_non_existing_track() throws InterruptedException {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
service.pauseCurrentTrack();
|
||||
Thread.sleep(2000);
|
||||
|
||||
// then
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
|
||||
|
||||
// when
|
||||
service.resumeCurrentTrack();
|
||||
Thread.sleep(1000);
|
||||
service.resumeTrack(new Track.Id(-1));
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void cannotEnd_without_starting() throws InterruptedException {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
|
||||
// when
|
||||
service.endCurrentTrack();
|
||||
@@ -257,8 +245,7 @@ public class TrackRecordingServiceTestStateMachine {
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertFalse(service.isPaused());
|
||||
assertEquals(new RecordingStatus(trackId, false), service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
|
||||
assertNotEquals(TrackRecordingService.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
|
||||
@@ -27,7 +27,7 @@ public class TrackRecordingServiceTestUtils {
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
||||
.getService();
|
||||
|
||||
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, service.getTrackPointCreator()));
|
||||
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, service.getTrackPointCreator()));
|
||||
service.getTrackPointCreator().setClock(Clock.systemUTC());
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,13 @@ public class VoiceAnnouncementUtilsTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
PreferencesUtils.setVoiceAnnounceHeartRate(false);
|
||||
|
||||
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
|
||||
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
|
||||
PreferencesUtils.setVoiceAnnounceTotalDistance(true);
|
||||
PreferencesUtils.setVoiceAnnounceMovingTime(true);
|
||||
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(true);
|
||||
PreferencesUtils.setVoiceAnnounceLapSpeedPace(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -58,7 +64,55 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_speed_rounding_check() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_distance_rounding_check() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(19999));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_distance_rounding_check_two() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(19990));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 19.99 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,7 +134,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +188,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,7 +210,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,7 +253,8 @@ public class VoiceAnnouncementUtilsTest {
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_heart_rate_and_sensor_statistics() {
|
||||
PreferencesUtils.setVoiceAnnounceHeartRate(true);
|
||||
PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
|
||||
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
|
||||
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
@@ -219,6 +274,36 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
|
||||
assertEquals("Total distance 14.21 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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_only_lap_heart_rate() {
|
||||
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
|
||||
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
|
||||
PreferencesUtils.setVoiceAnnounceTotalDistance(false);
|
||||
PreferencesUtils.setVoiceAnnounceMovingTime(false);
|
||||
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false);
|
||||
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
|
||||
|
||||
int numberOfPoints = 1000;
|
||||
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
|
||||
Track.Id trackId = trackWithStats.first;
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
}
|
||||
|
||||
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals(" Current heart rate 133 bpm.", announcement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class TrackPointCreatorTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
subject = new TrackPointCreator(locationHandler, server);
|
||||
subject.start(context);
|
||||
subject.start(context, null);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -58,7 +58,7 @@ public class PreferencesUtilsTest {
|
||||
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
|
||||
|
||||
// then
|
||||
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
|
||||
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, trackFileFormat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,7 +73,7 @@ public class PreferencesUtilsTest {
|
||||
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
|
||||
|
||||
// then
|
||||
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
|
||||
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, trackFileFormat);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -99,7 +99,7 @@ public class DescriptionGeneratorTest {
|
||||
+ "Min elevation: -500 m (-1640 ft)<br>"
|
||||
+ "Elevation gain: 6000 m (19685 ft)<br>"
|
||||
+ "Elevation loss: 6000 m (19685 ft)<br>"
|
||||
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class DescriptionGeneratorTest {
|
||||
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
|
||||
+ "Elevation gain: 6000 m (19685 ft)<br>"
|
||||
+ "Elevation loss: 6000 m (19685 ft)<br>"
|
||||
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
@@ -86,7 +87,7 @@ public class TrackStatisticsUpdaterTest {
|
||||
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
|
||||
|
||||
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
|
||||
assertEquals(28.75, statistics.getMaxAltitude(), 0.01);
|
||||
assertEquals(32.5, statistics.getMaxAltitude(), 0.01);
|
||||
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
|
||||
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
|
||||
|
||||
@@ -245,6 +246,70 @@ public class TrackStatisticsUpdaterTest {
|
||||
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_maxSpeed_ignore_above_acceleration() {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
subject.addTrackPoint(new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)));
|
||||
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// Ignore as we set max speed if two consecutive trackpoints were considered moving
|
||||
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
|
||||
.setSpeed(Speed.of(1f)));
|
||||
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// Update max speed
|
||||
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
|
||||
.setSpeed(Speed.of(1f)));
|
||||
assertEquals(Speed.of(1f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// Update max speed
|
||||
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(12))
|
||||
.setSpeed(Speed.of(50f)));
|
||||
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// Ignore; we were getting slower
|
||||
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(13))
|
||||
.setSpeed(Speed.of(5f)));
|
||||
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// Ignore acceleration above 2g
|
||||
subject.addTrackPoint(new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(14))
|
||||
.setSpeed(Speed.of(500f)));
|
||||
assertEquals(Speed.of(50f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addTrackPoint_maxSpeed_multiple_segments() {
|
||||
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||
assertEquals(Speed.of(0f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
subject.addTrackPoints(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)),
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(1))
|
||||
.setSpeed(Speed.of(2f)),
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(2))
|
||||
.setSpeed(Speed.of(2f)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(4))
|
||||
));
|
||||
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
|
||||
|
||||
// when
|
||||
subject.addTrackPoints(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(5)),
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(6))
|
||||
.setSpeed(Speed.of(1f)),
|
||||
new TrackPoint(0, 0, Altitude.WGS84.of(0), Instant.ofEpochSecond(7))
|
||||
.setSpeed(Speed.of(1f)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochSecond(8))
|
||||
));
|
||||
|
||||
// then
|
||||
assertEquals(Speed.of(2f), subject.getTrackStatistics().getMaxSpeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copy_constructor() {
|
||||
// given
|
||||
|
||||
@@ -59,13 +59,13 @@ public class EspressoAggregatedFilterTest {
|
||||
@Test
|
||||
public void espressoAggregatedFilterTest() {
|
||||
// open AggregatedStatisticsActivity through toolbar's menu item
|
||||
onView(ViewMatchers.withId(R.id.toolbar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
|
||||
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
|
||||
onView(withId(R.id.track_list_aggregated_stats)).perform(click());
|
||||
|
||||
// open FilterDialogFragment through toolbar's menu item
|
||||
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
|
||||
|
||||
// check there's a checkbox with CATEGORY text
|
||||
|
||||
@@ -3,10 +3,10 @@ package de.dennisguse.opentracks.ui.customRecordingLayout;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
@@ -41,7 +41,7 @@ public class EspressoCustomLayoutTest {
|
||||
@Test
|
||||
public void customLayoutTest() {
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(ViewMatchers.withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(ViewMatchers.withId(R.id.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
|
||||
// Get custom layout preferences and check all data fields are showed.
|
||||
@@ -50,8 +50,8 @@ public class EspressoCustomLayoutTest {
|
||||
onView(withId(R.id.stats_recycler_view)).check(new RecyclerViewItemCountAssertion((int) layout.getFields().stream().filter(DataField::isVisible).count()));
|
||||
|
||||
// stop recording
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
trackControllerStopButton.perform(longClick());
|
||||
}
|
||||
|
||||
private static class RecyclerViewItemCountAssertion implements ViewAssertion {
|
||||
|
||||
@@ -21,7 +21,6 @@ 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.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
|
||||
@@ -180,7 +179,7 @@ public class IntervalStatisticsTest {
|
||||
|
||||
// then
|
||||
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(trackStatistics.getTotalTime().toSeconds(), totalTime, 0.01);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
|
||||
if (totalGain != null) {
|
||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
|
||||
@@ -142,7 +142,7 @@ public class EGM2008UtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Berlin() throws IOException {
|
||||
public void data_Berlin_Germany_() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint.setLatitude(52.530644);
|
||||
@@ -157,7 +157,7 @@ public class EGM2008UtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Berlin_Caching() throws IOException {
|
||||
public void data_Berlin_Germany_Caching() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
trackPoint1.setLatitude(52.530644);
|
||||
@@ -176,6 +176,22 @@ public class EGM2008UtilsTest {
|
||||
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_Seattle_USA() throws IOException {
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
|
||||
trackPoint.setLatitude(47.63153);
|
||||
trackPoint.setLongitude(-122.30938);
|
||||
trackPoint.setAltitude(0);
|
||||
|
||||
// when
|
||||
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
|
||||
|
||||
// then
|
||||
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void data_MaxUndulation() throws IOException {
|
||||
// given
|
||||
@@ -191,6 +207,7 @@ public class EGM2008UtilsTest {
|
||||
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void getIndices() {
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
|
||||
@@ -208,7 +225,7 @@ public class EGM2008UtilsTest {
|
||||
|
||||
trackPoint.setLatitude(-90);
|
||||
trackPoint.setLongitude(-180);
|
||||
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
assertEquals(new EGM2008Utils.Indices(2160, 2160), EGM2008Utils.getIndices(trackPoint.getLocation()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.espresso.action.MotionEvents;
|
||||
import androidx.test.espresso.util.HumanReadables;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
@@ -23,8 +16,6 @@ import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class EspressoUtils {
|
||||
|
||||
public static Matcher<View> childAtPosition(
|
||||
@@ -80,76 +71,6 @@ public class EspressoUtils {
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction veryLongTouch(final int duration_ms) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Perform long touch.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
// Get view absolute position
|
||||
int[] location = new int[2];
|
||||
view.getLocationOnScreen(location);
|
||||
|
||||
// Offset coordinates by view position
|
||||
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
|
||||
|
||||
// Send down event, pause, and send up
|
||||
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
MotionEvents.sendUp(uiController, down, coordinates);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction waitId(final int viewId, final long millis) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(final UiController uiController, final View view) {
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final long endTime = startTime + millis;
|
||||
final Matcher<View> viewMatcher = withId(viewId);
|
||||
|
||||
do {
|
||||
for (View child : breadthFirstViewTraversal(view)) {
|
||||
// found view with required ID
|
||||
if (viewMatcher.matches(child)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uiController.loopMainThreadForAtLeast(50);
|
||||
}
|
||||
while (System.currentTimeMillis() < endTime);
|
||||
|
||||
// timeout happens
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(this.getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(new TimeoutException())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction selectTabAtIndex(final int index) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class ExportUtilsTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testIsExportFileExists_exists() {
|
||||
// given
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String format = "kmz";
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
|
||||
fileNames.add(uuid.toString().substring(0, 8) + "_other name name.kmz");
|
||||
|
||||
// when
|
||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
||||
|
||||
// then
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsExportFileExists_not_exists1() {
|
||||
// given
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String format = "gpx";
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_name.gpx");
|
||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
|
||||
|
||||
// when
|
||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
||||
|
||||
// then
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsExportFileExists_not_exists2() {
|
||||
// given
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String format = "kmz";
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
fileNames.add(uuid.toString().substring(0, 8) + "_name.gpx");
|
||||
fileNames.add(UUID.randomUUID().toString().substring(0, 8) + "_other name name.gpx");
|
||||
|
||||
// when
|
||||
boolean exists = ExportUtils.isExportFileExists(uuid, format, fileNames);
|
||||
|
||||
// then
|
||||
assertFalse(exists);
|
||||
}
|
||||
}
|
||||
@@ -41,12 +41,12 @@ import java.time.format.TextStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.TimezoneRule;
|
||||
|
||||
/**
|
||||
* Tests for {@link StringUtils}.
|
||||
@@ -59,6 +59,9 @@ public class StringUtilsTest {
|
||||
@Rule
|
||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||
|
||||
@Rule
|
||||
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
@@ -99,51 +102,13 @@ public class StringUtilsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTime_fractional() {
|
||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
}
|
||||
public void testParseTime() {
|
||||
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352").toInstant());
|
||||
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352Z").toInstant());
|
||||
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T00:00:00.352+00:00").toInstant());
|
||||
|
||||
@Test
|
||||
public void testGetTime_timezone() {
|
||||
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0);
|
||||
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTime_fractionalAndTimezone() {
|
||||
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470);
|
||||
assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579);
|
||||
assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the {@link StringUtils#parseTime(String)} returns the expected values.
|
||||
*
|
||||
* @param xmlDateTime the xml date time string
|
||||
* @param year the expected year
|
||||
* @param month the expected month
|
||||
* @param day the expected day
|
||||
* @param hour the expected hour
|
||||
* @param minute the expected minute
|
||||
* @param second the expected second
|
||||
* @param millisecond the expected milliseconds
|
||||
*/
|
||||
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
|
||||
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
calendar.set(year, month - 1, day, hour, minute, second);
|
||||
calendar.set(GregorianCalendar.MILLISECOND, millisecond);
|
||||
|
||||
// This comparision tends to be flaky (difference of 1ms)
|
||||
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
|
||||
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime).toInstant().toEpochMilli()) <= 1);
|
||||
assertEquals(Instant.ofEpochMilli(352), StringUtils.parseTime("1970-01-01T01:00:00.352+01:00").toInstant());
|
||||
assertEquals(Instant.ofEpochMilli(352).plus(Duration.ofHours(1)), StringUtils.parseTime("1970-01-01T00:00:00.352-01:00").toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx
|
||||
version="1.1"
|
||||
creator="OpenTracks (Debug)"
|
||||
xmlns="http://www.topografix.com/GPX/1/1"
|
||||
xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
|
||||
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
|
||||
xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
|
||||
xmlns:cluetrust="http://www.cluetrust.com/Schemas/"
|
||||
xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1"
|
||||
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackStatsExtension/v1 http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
|
||||
<wpt lat="3" lon="14">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:51:59.179+01:15</time>
|
||||
<name><![CDATA[Marker 1]]></name>
|
||||
<desc><![CDATA[Marker 1 desc]]></desc>
|
||||
<type><![CDATA[Marker 1 category]]></type>
|
||||
</wpt>
|
||||
<trk>
|
||||
<name><![CDATA[2021-01-07 22:51]]></name>
|
||||
<desc><![CDATA[the description]]></desc>
|
||||
<type><![CDATA[the category]]></type>
|
||||
<extensions>
|
||||
<topografix:color>c0c0c0</topografix:color>
|
||||
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
|
||||
<gpxtrkx:TrackStatsExtension>
|
||||
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
|
||||
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
|
||||
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
|
||||
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
|
||||
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
|
||||
<gpxtrkx:Ascent>null</gpxtrkx:Ascent>
|
||||
<gpxtrkx:Descent>null</gpxtrkx:Descent>
|
||||
</gpxtrkx:TrackStatsExtension>
|
||||
</extensions>
|
||||
<trkseg>
|
||||
<trkpt lat="3" lon="14">
|
||||
<ele>10</ele>
|
||||
<time>2021-01-07T21:51:59.179+01:15</time>
|
||||
</trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.dennisguse.opentracks"
|
||||
android:installLocation="auto">
|
||||
|
||||
<supports-screens
|
||||
@@ -59,6 +58,8 @@ limitations under the License.
|
||||
tools:targetApi="s" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".Startup"
|
||||
android:allowBackup="false"
|
||||
@@ -77,14 +78,14 @@ limitations under the License.
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- <activity-->
|
||||
<!-- android:name=".publicapi.StopRecording"-->
|
||||
<!-- android:exported="true"-->
|
||||
<!-- android:theme="@style/SplashTheme">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </activity>-->
|
||||
<activity
|
||||
android:name=".publicapi.StopRecording"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
||||
@@ -324,6 +325,7 @@ limitations under the License.
|
||||
</activity>
|
||||
|
||||
<activity android:name=".TrackRecordedActivity" />
|
||||
<activity android:name=".TrackStoppedActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".TrackRecordingActivity"
|
||||
@@ -347,21 +349,12 @@ limitations under the License.
|
||||
<activity android:name=".settings.SettingsCustomLayoutEditActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".share.ShowOnMapProxyActivity$KML"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/open_track_as_KML">
|
||||
<action android:name="Intent.OpenTracks-Dashboard" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".share.ShowOnMapProxyActivity$GPX"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/open_track_as_GPX">
|
||||
<action android:name="Intent.OpenTracks-Dashboard" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:name=".ShowErrorActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:process=":crash"/>
|
||||
|
||||
<provider
|
||||
android:name=".data.ShareContentProvider"
|
||||
|
||||
@@ -24,6 +24,8 @@ public class AboutActivity extends AbstractActivity {
|
||||
viewBinding.aboutTextVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
|
||||
viewBinding.aboutAppUrl.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
|
||||
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ package de.dennisguse.opentracks;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncement;
|
||||
|
||||
@@ -38,9 +36,6 @@ public abstract class AbstractActivity extends AppCompatActivity {
|
||||
setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM);
|
||||
|
||||
setContentView(getRootView());
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setupActionBarBack(toolbar);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,12 +45,4 @@ public abstract class AbstractActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
protected abstract View getRootView();
|
||||
|
||||
protected void setupActionBarBack(@Nullable Toolbar toolbar) {
|
||||
setSupportActionBar(toolbar);
|
||||
if (toolbar != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +84,18 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
||||
/**
|
||||
* Called every time a track is deleted.
|
||||
*/
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||
if (trackDeleteServiceConnection != null) {
|
||||
trackDeleteServiceConnection.unbind(this);
|
||||
trackDeleteServiceConnection = null;
|
||||
onDeleteFinished();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void onDeleteConfirmed();
|
||||
|
||||
protected abstract void onDeleteFinished();
|
||||
|
||||
protected abstract Track.Id getRecordingTrackId();
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Track controller for record, pause, resume, and stop.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
//TODO Move buttons into separate components
|
||||
public class ControllerFragment extends Fragment implements View.OnTouchListener {
|
||||
|
||||
private static final String TAG = ControllerFragment.class.getSimpleName();
|
||||
|
||||
private Handler handlerUpdateTotalTime;
|
||||
|
||||
private Runnable buttonDelay;
|
||||
private int buttonDelayDuration;
|
||||
|
||||
private ControllerFragmentBinding viewBinding;
|
||||
private TransitionDrawable transition;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private RecordingStatus recordingStatus;
|
||||
private RecordingData recordingData;
|
||||
|
||||
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(ControllerFragment.this, this::onRecordingStatusChanged);
|
||||
service.getRecordingDataObservable()
|
||||
.observe(ControllerFragment.this, this::onTotalTimeChanged);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
handlerUpdateTotalTime = new Handler();
|
||||
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
viewBinding.controllerRecord.setOnTouchListener(this);
|
||||
viewBinding.controllerRecord.setOnClickListener((view) -> {
|
||||
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Callback callback = (Callback) getContext();
|
||||
if (callback != null) {
|
||||
callback.recordStart();
|
||||
}
|
||||
});
|
||||
|
||||
viewBinding.controllerStop.setOnTouchListener(this);
|
||||
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
|
||||
|
||||
onRecordingStatusChanged(recordingStatus);
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
handlerUpdateTotalTime.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
transition = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection = null;
|
||||
handlerUpdateTotalTime = null;
|
||||
recordingStatus = null;
|
||||
}
|
||||
|
||||
//TODO Move into a separate object
|
||||
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
||||
@Override
|
||||
public boolean onTouch(final View view, MotionEvent event) {
|
||||
if (!isResumed()) {
|
||||
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
|
||||
return false;
|
||||
}
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
||||
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
||||
buttonDelay = null;
|
||||
view.setPressed(false);
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
showStatusSetDefaultText();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Trigger button pressed animation
|
||||
view.setPressed(true);
|
||||
|
||||
//To pause a recording
|
||||
if (viewBinding.controllerRecord.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& recordingStatus.isRecordingAndNotPaused()) {
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 150);
|
||||
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
view.performClick();
|
||||
Callback callback = (Callback) getContext();
|
||||
if (callback != null) {
|
||||
callback.recordPause();
|
||||
}
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 1000);
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
//To stop a recording
|
||||
if (viewBinding.controllerStop.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& recordingStatus.isRecording()) {
|
||||
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(getContext(), 150);
|
||||
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
Context context = getContext();
|
||||
Callback callback = (Callback) context;
|
||||
if (callback != null) {
|
||||
callback.recordStop();
|
||||
ActivityUtils.vibrate(context, 1000);
|
||||
}
|
||||
|
||||
transition.resetTransition();
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
viewBinding.controllerContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void showStatusSetDefaultText() {
|
||||
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void recordStart();
|
||||
|
||||
void recordPause();
|
||||
|
||||
void recordStop();
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
|
||||
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
|
||||
|
||||
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
|
||||
|
||||
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||
if (recordingStatus.isRecording()) {
|
||||
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
|
||||
showStatusSetDefaultText();
|
||||
}
|
||||
|
||||
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void onTotalTimeChanged(RecordingData recordingData) {
|
||||
this.recordingData = recordingData;
|
||||
|
||||
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(this.recordingData.getTrackStatistics().getTotalTime()));
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,18 @@ import de.dennisguse.opentracks.ui.util.ViewUtils;
|
||||
|
||||
public class HelpActivity extends AbstractActivity {
|
||||
|
||||
private HelpBinding helpBinding;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setSupportActionBar(helpBinding.bottomAppBarLayout.bottomAppBar);
|
||||
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
return HelpBinding.inflate(getLayoutInflater()).getRoot();
|
||||
helpBinding = HelpBinding.inflate(getLayoutInflater());
|
||||
return helpBinding.getRoot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
|
||||
|
||||
public class ShowErrorActivity extends AbstractActivity {
|
||||
|
||||
public static final String EXTRA_ERROR_TEXT = "error";
|
||||
|
||||
private ActivityShowErrorBinding viewBinding;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
viewBinding.textViewError.setText(getIntent().getStringExtra(EXTRA_ERROR_TEXT));
|
||||
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(createErrorTitle());
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = ActivityShowErrorBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
private String createErrorTitle() {
|
||||
return String.format(getString(R.string.error_crash_title), getString(R.string.app_name));
|
||||
}
|
||||
|
||||
private void reportBug() {
|
||||
Uri uriUrl;
|
||||
try {
|
||||
uriUrl = Uri.parse(
|
||||
String.format(
|
||||
getString(R.string.report_issue_link),
|
||||
URLEncoder.encode(viewBinding.textViewError.getText().toString(), StandardCharsets.UTF_8.toString())
|
||||
)
|
||||
);
|
||||
} catch (final UnsupportedEncodingException ignored) {
|
||||
// can't happen as UTF-8 is always available
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uriUrl);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.show_error, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (item.getItemId() == R.id.error_share) {
|
||||
onClickedShare();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.error_report) {
|
||||
reportBug();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void onClickedShare() {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, createErrorTitle());
|
||||
intent.putExtra(Intent.EXTRA_TEXT, viewBinding.textViewError.getText());
|
||||
intent.setType("text/plain");
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.ExceptionHandler;
|
||||
|
||||
/**
|
||||
* Code that is executed when the application starts.
|
||||
@@ -37,6 +43,36 @@ public class Startup extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
// handle crashes only outside the crash reporter activity/process
|
||||
if (!isCrashReportingProcess()) {
|
||||
Thread.UncaughtExceptionHandler defaultPlatformHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
ExceptionHandler crashReporter = new ExceptionHandler(this, defaultPlatformHandler);
|
||||
Thread.setDefaultUncaughtExceptionHandler(crashReporter);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCrashReportingProcess() {
|
||||
String processName = "";
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
// Using the same technique as Application.getProcessName() for older devices
|
||||
// Using reflection since ActivityThread is an internal API
|
||||
try {
|
||||
@SuppressLint("PrivateApi")
|
||||
Class<?> activityThread = Class.forName("android.app.ActivityThread");
|
||||
@SuppressLint("DiscouragedPrivateApi") Method getProcessName = activityThread.getDeclaredMethod("currentProcessName");
|
||||
processName = (String) getProcessName.invoke(null);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
} else {
|
||||
processName = Application.getProcessName();
|
||||
}
|
||||
return processName != null && processName.endsWith(":crash");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the database used by SQLiteOpenHelper.
|
||||
* See {@link android.database.sqlite.SQLiteOpenHelper} for details.
|
||||
|
||||
@@ -71,17 +71,17 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
return;
|
||||
}
|
||||
|
||||
viewBinding.trackEditName.setText(track.getName());
|
||||
viewBinding.fields.trackEditName.setText(track.getName());
|
||||
|
||||
viewBinding.trackEditActivityType.setText(track.getCategory());
|
||||
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
viewBinding.trackEditActivityType.setAdapter(adapter);
|
||||
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
|
||||
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
|
||||
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
|
||||
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
setActivityTypeIcon(TrackIconUtils.getIconValue(
|
||||
TrackEditActivity.this, viewBinding.trackEditActivityType.getText().toString()));
|
||||
TrackEditActivity.this, viewBinding.fields.trackEditActivityType.getText().toString()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,22 +94,23 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
}
|
||||
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
|
||||
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
|
||||
|
||||
viewBinding.trackEditDescription.setText(track.getDescription());
|
||||
viewBinding.fields.trackEditDescription.setText(track.getDescription());
|
||||
|
||||
viewBinding.trackEditSave.setOnClickListener(v -> {
|
||||
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
||||
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
|
||||
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.fields.trackEditName.getText().toString(),
|
||||
viewBinding.fields.trackEditActivityType.getText().toString(), viewBinding.fields.trackEditDescription.getText().toString(),
|
||||
contentProviderUtils);
|
||||
finish();
|
||||
});
|
||||
|
||||
setTitle(R.string.menu_edit);
|
||||
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
|
||||
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_edit));
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
@@ -137,12 +138,12 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
|
||||
private void setActivityTypeIcon(String iconValue) {
|
||||
this.iconValue = iconValue;
|
||||
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String value) {
|
||||
setActivityTypeIcon(value);
|
||||
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,20 @@ import android.database.Cursor;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
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.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
@@ -68,6 +71,7 @@ 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;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -76,7 +80,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
|
||||
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
|
||||
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
|
||||
@@ -101,6 +105,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
|
||||
boolean isSingleSelection = trackIds.length == 1;
|
||||
|
||||
viewBinding.bottomAppBar.performHide(true);
|
||||
viewBinding.trackListFabAction.setVisibility(View.INVISIBLE);
|
||||
|
||||
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
|
||||
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
|
||||
}
|
||||
@@ -109,6 +116,12 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
public boolean onClick(int itemId, int[] positions, long[] trackIds) {
|
||||
return handleContextItem(itemId, trackIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
viewBinding.trackListFabAction.setVisibility(View.VISIBLE);
|
||||
viewBinding.bottomAppBar.performShow(true);
|
||||
}
|
||||
};
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
@@ -127,7 +140,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
private MenuItem searchMenuItem;
|
||||
private MenuItem startGpsMenuItem;
|
||||
|
||||
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
|
||||
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(TrackListActivity.this, this::onRecordingStatusChanged);
|
||||
|
||||
@@ -146,25 +159,27 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Reset theme after splash
|
||||
setTheme(R.style.ThemeCustom);
|
||||
SplashScreen.installSplashScreen(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
requestRequiredPermissions();
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
|
||||
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||
if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
|
||||
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, new Track.Id(trackId));
|
||||
.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, new Track.Id(trackId));
|
||||
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
|
||||
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
|
||||
this,
|
||||
new Pair<>(view.findViewById(R.id.list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON));
|
||||
@@ -201,7 +216,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording,
|
||||
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
|
||||
OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)),
|
||||
category, description, false);
|
||||
@@ -210,13 +225,45 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
viewBinding.trackList.setAdapter(resourceCursorAdapter);
|
||||
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
|
||||
|
||||
viewBinding.trackListFabAction.setOnClickListener((view) -> {
|
||||
if (recordingStatus.isRecording()) {
|
||||
Toast.makeText(TrackListActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Not Recording -> Recording
|
||||
updateGpsMenuItem(false, true);
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||
newIntent.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
|
||||
connection.unbind(this);
|
||||
}).startAndBind(this, true);
|
||||
});
|
||||
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
|
||||
if (!recordingStatus.isRecording()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recording -> Stop
|
||||
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
|
||||
updateGpsMenuItem(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
|
||||
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.red_dark));
|
||||
return true;
|
||||
});
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBar);
|
||||
|
||||
loadData(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupActionBarBack(Toolbar toolbar) {
|
||||
setSupportActionBar(toolbar);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
|
||||
private void requestRequiredPermissions() {
|
||||
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -234,6 +281,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
||||
|
||||
// Float button
|
||||
setFloatButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,6 +313,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
|
||||
|
||||
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
|
||||
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
|
||||
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
|
||||
|
||||
searchView.setOnCloseListener(() -> {
|
||||
searchView.clearFocus();
|
||||
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
@@ -382,6 +439,11 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteFinished() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
@@ -422,7 +484,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
}
|
||||
|
||||
if (itemId == R.id.list_context_menu_show_on_map) {
|
||||
IntentDashboardUtils.startDashboard(this, false, trackIds);
|
||||
IntentDashboardUtils.showTrackOnMap(this, false, trackIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -515,38 +577,18 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStart() {
|
||||
if (recordingStatus.getTrackId() == null) {
|
||||
// Not recording -> Recording
|
||||
updateGpsMenuItem(false, true);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||
startActivity(newIntent);
|
||||
} else if (recordingStatus.isPaused()) {
|
||||
// Paused -> Resume
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordPause() {
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStop() {
|
||||
updateGpsMenuItem(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||
}
|
||||
|
||||
public void onGpsStatusChanged(GpsStatusValue newStatus) {
|
||||
gpsStatusValue = newStatus;
|
||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||
}
|
||||
|
||||
private void setFloatButton() {
|
||||
viewBinding.trackListFabAction.setImageResource(recordingStatus.isRecording() ? R.drawable.ic_baseline_stop_24 : R.drawable.ic_baseline_record_24);
|
||||
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, recordingStatus.isRecording() ? R.color.opentracks_secondary_color : R.color.red_dark));
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
setFloatButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
|
||||
private final TrackRecordingServiceConnection.Callback bindCallback = service -> service.getRecordingStatusObservable()
|
||||
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
|
||||
.observe(TrackRecordedActivity.this, this::onRecordingStatusChanged);
|
||||
|
||||
@Override
|
||||
@@ -102,6 +102,11 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
|
||||
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(track != null ? track.getName() : "");
|
||||
viewBinding.bottomAppBarLayout.bottomAppBar.replaceMenu(R.menu.track_detail);
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
|
||||
postponeEnterTransition();
|
||||
}
|
||||
|
||||
@@ -162,8 +167,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
setTitle(track != null ? track.getName() : "");
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -176,7 +179,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
|
||||
IntentDashboardUtils.startDashboard(this, false, trackId);
|
||||
IntentDashboardUtils.showTrackOnMap(this, false, trackId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -200,11 +203,17 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_resume_track) {
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
finish();
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
connection.unbind(this);
|
||||
finish();
|
||||
}).startAndBind(this, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -223,9 +232,9 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
super.onTrackDeleteStatus(deleteStatus);
|
||||
if (deleteStatus.isDeleted(trackId)) {
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||
super.onTrackDeleteStatus(deletionFinishedStatus);
|
||||
if (deletionFinishedStatus.isDeleted(trackId)) {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
}
|
||||
@@ -235,6 +244,11 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteFinished() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link TrackDataHub}.
|
||||
*/
|
||||
@@ -246,7 +260,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
private void handleIntent(Intent intent) {
|
||||
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||
Log.e(TAG, TrackRecordedActivity.class.getSimpleName() + " needs EXTRA_TRACK_ID.");
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -13,9 +14,11 @@ import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
@@ -44,7 +47,6 @@ import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
|
||||
@@ -54,9 +56,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
||||
//This mode of communication should be removed anyhow.
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackDataHubInterface, ControllerFragment.Callback {
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackDataHubInterface {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
@@ -73,13 +73,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
|
||||
private TrackRecordingBinding viewBinding;
|
||||
|
||||
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
|
||||
@Deprecated
|
||||
private Track.Id trackId;
|
||||
|
||||
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
|
||||
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
|
||||
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
|
||||
service.getRecordingStatusObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onRecordingStatusChanged);
|
||||
|
||||
@@ -87,18 +85,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
||||
|
||||
if (!service.isRecording()) {
|
||||
if (trackId == null) {
|
||||
// trackId isn't initialized -> leads a new recording.
|
||||
trackId = service.startNewTrack();
|
||||
} else {
|
||||
// trackId is initialized -> resumes the track.
|
||||
service.resumeTrack(trackId);
|
||||
}
|
||||
|
||||
// A recording track is on.
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
};
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
@@ -116,24 +108,18 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu); //TODO Should not be necessary
|
||||
};
|
||||
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem markerListMenuItem;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
contentProviderUtils = new ContentProviderUtils(this);
|
||||
trackId = null;
|
||||
if (savedInstanceState != null) {
|
||||
//Activity was recreated.
|
||||
trackId = savedInstanceState.getParcelable(EXTRA_TRACK_ID);
|
||||
} else {
|
||||
// Resume a track
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
if (trackId != null && contentProviderUtils.getTrack(trackId) == null) {
|
||||
Log.w(TAG, "TrackId does not exists; cannot continue the recording.");
|
||||
finish();
|
||||
}
|
||||
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
if (trackId == null) {
|
||||
throw new RuntimeException("TrackId is mandatory");
|
||||
}
|
||||
if (contentProviderUtils.getTrack(trackId) == null) {
|
||||
Log.w(TAG, "TrackId does not exists.");
|
||||
finish();
|
||||
}
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
@@ -147,7 +133,25 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
requestGPSPermissions();
|
||||
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
|
||||
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
|
||||
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
|
||||
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
|
||||
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
|
||||
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackStoppedActivity.class)
|
||||
.putExtra(TrackStoppedActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
finish();
|
||||
return true;
|
||||
});
|
||||
viewBinding.trackRecordingFabAction.setOnClickListener((view) -> {
|
||||
Toast.makeText(TrackRecordingActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
viewBinding.bottomAppBar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
|
||||
setSupportActionBar(viewBinding.bottomAppBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,9 +185,20 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
private void setFullscreenPolicy() {
|
||||
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen();
|
||||
boolean fullscreen = PreferencesUtils.shouldUseFullscreen();
|
||||
|
||||
if (keepScreenOn) {
|
||||
WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(getWindow().getDecorView());
|
||||
if (windowInsetsController != null) {
|
||||
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
if (fullscreen) {
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
|
||||
} else {
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
@@ -220,7 +235,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
|
||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -247,23 +261,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_record, menu);
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
|
||||
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
updateMenuItems(recordingStatus.isPaused());
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
|
||||
IntentDashboardUtils.startDashboard(this, true, trackId);
|
||||
IntentDashboardUtils.showTrackOnMap(this, true, trackId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -315,15 +319,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
return trackDataHub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items.
|
||||
*/
|
||||
private void updateMenuItems(boolean isPaused) {
|
||||
insertMarkerMenuItem.setVisible(!isPaused);
|
||||
markerListMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
setTitle(getString(isPaused ? R.string.generic_paused : R.string.generic_recording));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
@@ -331,29 +326,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStart() {
|
||||
updateMenuItems(false);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordPause() {
|
||||
updateMenuItems(true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStop() {
|
||||
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
|
||||
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
updateMenuItems(true);
|
||||
finish();
|
||||
}
|
||||
|
||||
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
|
||||
@@ -399,6 +371,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||
if (!status.isRecording()) {
|
||||
finish();
|
||||
}
|
||||
recordingStatus = status;
|
||||
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
@@ -407,24 +382,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
setScreenOnPolicy();
|
||||
}
|
||||
|
||||
private void requestGPSPermissions() {
|
||||
if (PermissionUtils.hasGPSPermission(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityResultLauncher<String[]> locationPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
|
||||
Boolean fineLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
|
||||
Boolean coarseLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false);
|
||||
if (fineLocationGranted == null || !fineLocationGranted
|
||||
|| coarseLocationGranted == null || !coarseLocationGranted) {
|
||||
Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
);
|
||||
String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
locationPermissionRequest.launch(permissions);
|
||||
}
|
||||
|
||||
private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
|
||||
if (gpsStatusValue.isGpsStarted() && snackbar != null && snackbar.isShown()) {
|
||||
snackbar.dismiss();
|
||||
@@ -437,7 +394,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
.make(viewBinding.trackRecordingCoordinatorLayout,
|
||||
getString(R.string.gps_recording_status, getString(gpsStatusValue.message), getString(R.string.gps_recording_without_signal)),
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(getString(R.string.generic_dismiss), v -> {});
|
||||
.setAction(getString(R.string.generic_dismiss), v -> {
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
|
||||
public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
||||
|
||||
private static final String TAG = TrackStoppedActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
private TrackStoppedBinding viewBinding;
|
||||
|
||||
private Track.Id trackId;
|
||||
|
||||
private boolean isDiscarding = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "TrackStoppedActivity needs EXTRA_TRACK_ID.");
|
||||
finish();
|
||||
}
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
viewBinding.trackEditName.setText(track.getName());
|
||||
|
||||
viewBinding.trackEditActivityType.setText(track.getCategory());
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
viewBinding.trackEditActivityType.setAdapter(adapter);
|
||||
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
|
||||
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
setActivityTypeIcon(TrackIconUtils.getIconValue(
|
||||
TrackStoppedActivity.this, viewBinding.trackEditActivityType.getText().toString()));
|
||||
}
|
||||
});
|
||||
|
||||
String iconValue = track.getIcon();
|
||||
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
|
||||
|
||||
viewBinding.trackEditDescription.setText(track.getDescription());
|
||||
|
||||
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
|
||||
|
||||
{
|
||||
Pair<String, String> parts = SpeedFormatter.Builder()
|
||||
.setUnit(PreferencesUtils.getUnitSystem())
|
||||
.setReportSpeedOrPace(PreferencesUtils.isReportSpeed(track.getCategory()))
|
||||
.build(this)
|
||||
.getSpeedParts(track.getTrackStatistics().getAverageMovingSpeed());
|
||||
viewBinding.speed.setText(parts.first);
|
||||
viewBinding.speedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = DistanceFormatter.Builder()
|
||||
.setUnit(PreferencesUtils.getUnitSystem())
|
||||
.build(this)
|
||||
.getDistanceParts(track.getTrackStatistics().getTotalDistance());
|
||||
viewBinding.distance.setText(parts.first);
|
||||
viewBinding.distanceUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
viewBinding.finishButton.setOnClickListener(v -> {
|
||||
storeTrackMetaData(contentProviderUtils, track);
|
||||
ExportUtils.postWorkoutExport(this, trackId);
|
||||
finish();
|
||||
});
|
||||
|
||||
viewBinding.resumeButton.setOnClickListener(v -> {
|
||||
storeTrackMetaData(contentProviderUtils, track);
|
||||
resumeTrackAndFinish();
|
||||
});
|
||||
|
||||
viewBinding.discardButton.setOnClickListener(v -> ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackId));
|
||||
}
|
||||
|
||||
private void storeTrackMetaData(ContentProviderUtils contentProviderUtils, Track track) {
|
||||
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
||||
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
|
||||
contentProviderUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isDiscarding) {
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
resumeTrackAndFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackStoppedBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
private void setActivityTypeIcon(String iconValue) {
|
||||
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
|
||||
}
|
||||
|
||||
private void resumeTrackAndFinish() {
|
||||
new TrackRecordingServiceConnection((service, connection) -> {
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
Intent newIntent = IntentUtils.newIntent(TrackStoppedActivity.this, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
connection.unbind(this);
|
||||
finish();
|
||||
}).startAndBind(this, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteConfirmed() {
|
||||
isDiscarding = true;
|
||||
viewBinding.loadingLayout.loadingText.setText(getString(R.string.track_discarding));
|
||||
viewBinding.contentLinearLayout.setVisibility(View.GONE);
|
||||
viewBinding.loadingLayout.loadingIndeterminate.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteFinished() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,7 @@ import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.TrackDataHub;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||
@@ -188,9 +186,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
|
||||
if (isResumed()) {
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||
pendingPoints.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@ package de.dennisguse.opentracks.chart;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
@@ -29,20 +27,20 @@ public class ChartPoint {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
|
||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, boolean chartByDistance, UnitSystem unitSystem) {
|
||||
if (chartByDistance) {
|
||||
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
|
||||
} else {
|
||||
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
||||
}
|
||||
|
||||
if (smoothedAltitude != null) {
|
||||
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
|
||||
if (trackPoint.hasAltitude()) {
|
||||
altitude = Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem);
|
||||
}
|
||||
|
||||
if (smoothedSpeed != null) {
|
||||
speed = smoothedSpeed.to(unitSystem);
|
||||
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
|
||||
pace = smoothedSpeed.toPace(unitSystem).toSeconds() / 60d;
|
||||
}
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRate = (double) trackPoint.getHeartRate().getBPM();
|
||||
|
||||