mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-01 01:02:21 +02:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
liberapay: OpenTracks
|
||||
github: [rgmf, pstorch, dennisguse]
|
||||
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
||||
custom: [ https://liberapay.com/OpenTracks, https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore ]
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Data not recorded
|
||||
about: Somehow OpenTracks did not record GPS data
|
||||
title: ''
|
||||
labels: potential bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please describe the problem and how to reproduce it:**
|
||||
|
||||
...
|
||||
|
||||
**Checklist**
|
||||
|
||||
- [ ] Consulted [https://dontkillmyapp.com/]
|
||||
- [ ] Power management: battery optimization is disabled for OpenTracks
|
||||
|
||||
**Technical information**
|
||||
|
||||
- Device: [e.g. Nexus 7 (2013)]
|
||||
- OS: [e.g. LineageOS 15.1]
|
||||
- OpenTracks version: [e.g. v3.2.4]
|
||||
- OpenTracks commit id (only for nightly builds): [e.g. 4b333bb]
|
||||
@@ -1,9 +1,10 @@
|
||||
#https://github.com/marketplace/actions/android-emulator-runner
|
||||
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">
|
||||
@@ -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,20 +107,37 @@ 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`
|
||||
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
||||
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
||||
|
||||
For testing via adb: `adb shell am start -n "package/class"`
|
||||
|
||||
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_CATEGORY`, and `TRACK_DESCRIPTION`
|
||||
* 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.
|
||||
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or
|
||||
Bluetooth) were granted. If they are not granted`
|
||||
|
||||
## Dashboard API (incl. map)
|
||||
|
||||
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live
|
||||
tracks.
|
||||
|
||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which
|
||||
presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also
|
||||
used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track
|
||||
statistics on supported wearables.
|
||||
|
||||
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (
|
||||
e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)). However, this is rather
|
||||
slow and does not provide updates while recording.
|
||||
|
||||
The developer documentation is in [README_API.md](README_API.md).
|
||||
|
||||
|
||||
+10
-10
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
compileSdkVersion 32
|
||||
compileSdk 33
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
@@ -59,13 +59,13 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4874
|
||||
versionName "v4.0.0"
|
||||
versionCode 4981
|
||||
versionName "v4.0.4"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 32
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
@@ -117,12 +117,12 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.6.0'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
@@ -130,7 +130,7 @@ dependencies {
|
||||
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.6.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -5,6 +5,7 @@ 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.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
@@ -14,7 +15,6 @@ 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;
|
||||
|
||||
@@ -55,149 +55,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.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
// TrackRecordingActivity
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
// wait; stay recording
|
||||
.perform(waitFor(5000))
|
||||
// stop;
|
||||
.perform(longClick());
|
||||
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerStopButton.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.bottom_app_bar),
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,22 @@ 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.action.ViewActions.scrollTo;
|
||||
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;
|
||||
@@ -53,7 +49,7 @@ public class EspressoUITest {
|
||||
.perform(longClick());
|
||||
|
||||
// TrackStoppedActivity: resume
|
||||
onView(withId(R.id.resume_button))
|
||||
onView(allOf(withId(R.id.resume_button), isClickable()))
|
||||
.perform(click());
|
||||
|
||||
// TrackRecordingActivity: wait and then stop
|
||||
@@ -93,7 +89,7 @@ public class EspressoUITest {
|
||||
tabLayout.perform(waitFor(1000));
|
||||
|
||||
// stop
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
trackControllerStopButton.perform(longClick());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,36 +97,6 @@ public class EspressoUITest {
|
||||
@Test
|
||||
public void selectAndDeleteTrack() {
|
||||
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
|
||||
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(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);
|
||||
|
||||
@@ -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,10 +23,7 @@ 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, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
|
||||
@@ -36,9 +32,7 @@ public class ShareContentProviderTest {
|
||||
|
||||
@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"), shareURIandMIME.first);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+34
-29
@@ -155,26 +155,31 @@ 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.setClock("2020-02-02T02:02:18Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||
service.pauseCurrentTrack();
|
||||
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.setClock("2020-02-02T02:03:24Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -187,7 +192,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 +236,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 +350,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 +370,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 +378,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 +395,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);
|
||||
|
||||
+36
-2
@@ -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);
|
||||
@@ -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.debug.test.R.raw.gpx_timezone);
|
||||
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
|
||||
trackExporter.writeTrack(importedTrack, outputStream);
|
||||
|
||||
// then
|
||||
String expected = new BufferedReader(
|
||||
new InputStreamReader(inputStreamExpected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n")) + "\n";
|
||||
|
||||
assertEquals(expected, outputStream.toString()); //TODO inputStream.readAllBytes() ?
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
+45
-2
@@ -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() {
|
||||
|
||||
+19
-32
@@ -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());
|
||||
|
||||
+87
-2
@@ -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
|
||||
@@ -61,6 +67,54 @@ public class VoiceAnnouncementUtilsTest {
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_speed_rounding_check() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1).plusSeconds(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_distance_rounding_check() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(19999));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_metric_distance_rounding_check_two() {
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(19990));
|
||||
stats.setTotalTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
|
||||
stats.setMovingTime(Duration.ofHours(1));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAnnouncement_withInterval_metric_speed() {
|
||||
// given
|
||||
@@ -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);
|
||||
@@ -221,4 +276,34 @@ public class VoiceAnnouncementUtilsTest {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -51,7 +51,7 @@ public class EspressoCustomLayoutTest {
|
||||
|
||||
// stop recording
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||
trackControllerStopButton.perform(longClick());
|
||||
}
|
||||
|
||||
private static class RecyclerViewItemCountAssertion implements ViewAssertion {
|
||||
|
||||
@@ -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>
|
||||
@@ -347,23 +347,6 @@ limitations under the License.
|
||||
<activity android:name=".settings.SettingsCustomLayoutListActivity" />
|
||||
<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>
|
||||
|
||||
<activity
|
||||
android:name=".ShowErrorActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@@ -88,11 +88,14 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
||||
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
||||
trackDeleteServiceConnection.unbind(this);
|
||||
trackDeleteServiceConnection = null;
|
||||
onDeleteFinished();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void onDeleteConfirmed();
|
||||
|
||||
protected abstract void onDeleteFinished();
|
||||
|
||||
protected abstract Track.Id getRecordingTrackId();
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,6 +70,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.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -163,6 +164,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
requestRequiredPermissions();
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
|
||||
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||
@@ -259,6 +262,16 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
loadData(getIntent());
|
||||
}
|
||||
|
||||
private void requestRequiredPermissions() {
|
||||
if (!PermissionUtils.hasGPSPermission(this)) {
|
||||
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
if (!PermissionUtils.hasBluetoothPermissions(this)) {
|
||||
PermissionUtils.requestBluetoothPermission(this, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
@@ -432,6 +445,11 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteFinished() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
@@ -472,7 +490,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,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;
|
||||
}
|
||||
|
||||
@@ -244,6 +244,11 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleteFinished() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link TrackDataHub}.
|
||||
*/
|
||||
|
||||
@@ -11,11 +11,16 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
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;
|
||||
@@ -131,8 +136,6 @@ 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(getResources().getColor(R.color.opentracks_secondary_color));
|
||||
@@ -185,9 +188,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);
|
||||
@@ -256,7 +270,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -371,14 +385,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
setScreenOnPolicy();
|
||||
}
|
||||
|
||||
private void requestGPSPermissions() {
|
||||
if (PermissionUtils.hasGPSPermission(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
|
||||
if (gpsStatusValue.isGpsStarted() && snackbar != null && snackbar.isShown()) {
|
||||
snackbar.dismiss();
|
||||
|
||||
@@ -15,13 +15,14 @@ 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 AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
||||
public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
||||
|
||||
private static final String TAG = TrackStoppedActivity.class.getSimpleName();
|
||||
|
||||
@@ -31,6 +32,8 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
|
||||
|
||||
private Track.Id trackId;
|
||||
|
||||
private boolean isDiscarding = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -44,26 +47,26 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
viewBinding.fields.trackEditName.setText(track.getName());
|
||||
viewBinding.trackEditName.setText(track.getName());
|
||||
|
||||
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
|
||||
viewBinding.trackEditActivityType.setText(track.getCategory());
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
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) -> {
|
||||
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.fields.trackEditActivityType.getText().toString()));
|
||||
TrackStoppedActivity.this, viewBinding.trackEditActivityType.getText().toString()));
|
||||
}
|
||||
});
|
||||
|
||||
String iconValue = track.getIcon();
|
||||
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
|
||||
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
|
||||
|
||||
viewBinding.fields.trackEditDescription.setText(track.getDescription());
|
||||
viewBinding.trackEditDescription.setText(track.getDescription());
|
||||
|
||||
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
|
||||
|
||||
@@ -87,20 +90,30 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
|
||||
}
|
||||
|
||||
viewBinding.finishButton.setOnClickListener(v -> {
|
||||
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.fields.trackEditName.getText().toString(),
|
||||
viewBinding.fields.trackEditActivityType.getText().toString(), viewBinding.fields.trackEditDescription.getText().toString(),
|
||||
contentProviderUtils);
|
||||
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();
|
||||
}
|
||||
@@ -112,13 +125,13 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
|
||||
}
|
||||
|
||||
private void setActivityTypeIcon(String iconValue) {
|
||||
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
|
||||
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
|
||||
}
|
||||
|
||||
private void resumeTrackAndFinish() {
|
||||
@@ -134,4 +147,22 @@ public class TrackStoppedActivity extends AbstractActivity implements ChooseActi
|
||||
finish();
|
||||
}).startAndBind(this);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +188,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ 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) {
|
||||
|
||||
@@ -44,6 +44,7 @@ abstract class ChartValueSeries {
|
||||
private final int[] intervalValues;
|
||||
private final int metricTitleId;
|
||||
private final int imperialTitleId;
|
||||
private final int nauticalTitleId;
|
||||
private final Paint fillPaint;
|
||||
private final Paint strokePaint;
|
||||
private final Paint titlePaint;
|
||||
@@ -57,24 +58,13 @@ abstract class ChartValueSeries {
|
||||
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param absoluteMin the absolute min value
|
||||
* @param absoluteMax the absolute max value
|
||||
* @param intervalValues the list of interval values
|
||||
* @param metricTitleId the metric title id
|
||||
* @param imperialTitleId the imperial title id
|
||||
* @param fillColor the fill color
|
||||
* @param strokeColor the stroke color
|
||||
*/
|
||||
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
|
||||
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int nauticalTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
|
||||
this.absoluteMin = absoluteMin;
|
||||
this.absoluteMax = absoluteMax;
|
||||
this.intervalValues = intervalValues;
|
||||
this.metricTitleId = metricTitleId;
|
||||
this.imperialTitleId = imperialTitleId;
|
||||
this.nauticalTitleId = nauticalTitleId;
|
||||
|
||||
fillPaint = new Paint();
|
||||
fillPaint.setStyle(Style.FILL);
|
||||
@@ -224,39 +214,27 @@ abstract class ChartValueSeries {
|
||||
return metricTitleId;
|
||||
case IMPERIAL:
|
||||
return imperialTitleId;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return nauticalTitleId;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title paint.
|
||||
*/
|
||||
Paint getTitlePaint() {
|
||||
return titlePaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the marker paint.
|
||||
*/
|
||||
Paint getMarkerPaint() {
|
||||
return markerPaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the largest marker.
|
||||
*/
|
||||
String getLargestMarker() {
|
||||
String minMarker = numberFormat.format(getMinMarkerValue());
|
||||
String maxMarker = numberFormat.format(getMaxMarkerValue());
|
||||
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a marker value.
|
||||
*
|
||||
* @param value the value
|
||||
*/
|
||||
String formatMarker(int value) {
|
||||
return numberFormat.format(value);
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ public class ChartView extends View {
|
||||
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
||||
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,
|
||||
fontSizeSmall,
|
||||
@@ -224,6 +225,7 @@ public class ChartView extends View {
|
||||
new int[]{1, 5, 10, 20, 50, 100},
|
||||
R.string.description_speed_metric,
|
||||
R.string.description_speed_imperial,
|
||||
R.string.description_speed_nautical,
|
||||
R.color.chart_speed_fill,
|
||||
R.color.chart_speed_border,
|
||||
fontSizeSmall,
|
||||
@@ -246,6 +248,7 @@ public class ChartView extends View {
|
||||
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
|
||||
R.string.description_pace_metric,
|
||||
R.string.description_pace_imperial,
|
||||
R.string.description_pace_nautical,
|
||||
R.color.chart_pace_fill,
|
||||
R.color.chart_pace_border,
|
||||
fontSizeSmall,
|
||||
@@ -268,6 +271,7 @@ public class ChartView extends View {
|
||||
new int[]{25, 50},
|
||||
R.string.description_sensor_heart_rate,
|
||||
R.string.description_sensor_heart_rate,
|
||||
R.string.description_sensor_heart_rate,
|
||||
R.color.chart_heart_rate_fill,
|
||||
R.color.chart_heart_rate_border,
|
||||
fontSizeSmall,
|
||||
@@ -289,6 +293,7 @@ public class ChartView extends View {
|
||||
new int[]{5, 10, 25, 50},
|
||||
R.string.description_sensor_cadence,
|
||||
R.string.description_sensor_cadence,
|
||||
R.string.description_sensor_cadence,
|
||||
R.color.chart_cadence_fill,
|
||||
R.color.chart_cadence_border,
|
||||
fontSizeSmall,
|
||||
@@ -309,6 +314,7 @@ public class ChartView extends View {
|
||||
new int[]{5, 50, 100, 200},
|
||||
R.string.description_sensor_power,
|
||||
R.string.description_sensor_power,
|
||||
R.string.description_sensor_power,
|
||||
R.color.chart_power_fill,
|
||||
R.color.chart_power_border,
|
||||
fontSizeSmall,
|
||||
@@ -732,6 +738,8 @@ public class ChartView extends View {
|
||||
return context.getString(R.string.unit_kilometer);
|
||||
case IMPERIAL:
|
||||
return context.getString(R.string.unit_mile);
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return context.getString(R.string.unit_nautical_mile);
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
@@ -72,9 +73,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
* @return An URI for one file containing this tracks.
|
||||
*/
|
||||
public static Pair<Uri, String> createURI(Track.Id trackId, String trackName, @NonNull TrackFileFormat trackFileFormat) {
|
||||
Set<Track.Id> trackIds = new HashSet<>(1);
|
||||
trackIds.add(trackId);
|
||||
return createURI(trackIds, trackName, trackFileFormat);
|
||||
return createURI(Set.of(trackId), trackName, trackFileFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,11 +107,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
|
||||
|
||||
Set<Track.Id> trackIds = new HashSet<>();
|
||||
for (String uriTrackId : uriTrackIds) {
|
||||
trackIds.add(new Track.Id(Long.parseLong(uriTrackId)));
|
||||
}
|
||||
return trackIds;
|
||||
return Arrays.stream(uriTrackIds).map(uriTrackId -> new Track.Id(Long.parseLong(uriTrackId))).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,14 +25,12 @@ import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
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;
|
||||
@@ -367,7 +365,7 @@ public class TrackDataHub {
|
||||
// Also include the last point if the selected track is not recording.
|
||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
for (Listener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||
}
|
||||
} else {
|
||||
for (Listener trackDataListener : listeners) {
|
||||
@@ -432,7 +430,7 @@ public class TrackDataHub {
|
||||
*
|
||||
* @param trackPoint the trackPoint
|
||||
*/
|
||||
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
|
||||
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,10 @@ public class Distance {
|
||||
return of(distance_mile * UnitConversions.MI_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofNauticalMile(double distance_mile) {
|
||||
return of(distance_mile * UnitConversions.NAUTICAL_MILE_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofKilometer(double distance_km) {
|
||||
return of(distance_km * UnitConversions.KM_TO_M);
|
||||
}
|
||||
@@ -51,6 +55,8 @@ public class Distance {
|
||||
return Distance.ofKilometer(1);
|
||||
case IMPERIAL:
|
||||
return Distance.ofMile(1);
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return Distance.ofNauticalMile(1);
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
@@ -114,12 +120,18 @@ public class Distance {
|
||||
return toKM() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public double toNauticalMiles() {
|
||||
return toKM() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||
}
|
||||
|
||||
public double toKM_Miles(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return toKM();
|
||||
case IMPERIAL:
|
||||
return toMI();
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return toNauticalMiles();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
@@ -129,6 +141,7 @@ public class Distance {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return toM();
|
||||
case NAUTICAL_IMPERIAL:
|
||||
case IMPERIAL:
|
||||
return toFT();
|
||||
default:
|
||||
|
||||
@@ -46,6 +46,7 @@ public class DistanceFormatter {
|
||||
case METRIC:
|
||||
return new Pair<>(null, resources.getString(R.string.unit_meter));
|
||||
case IMPERIAL:
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return new Pair<>(null, resources.getString(R.string.unit_feet));
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
@@ -65,6 +66,12 @@ public class DistanceFormatter {
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
case NAUTICAL_IMPERIAL:
|
||||
if (distance.greaterThan(Distance.ofNauticalMile(0.5))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ public class Speed {
|
||||
return toKMH() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public double toKnots() {
|
||||
return toKMH() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||
}
|
||||
|
||||
public Duration toPace(UnitSystem unitSystem) {
|
||||
if (isZero()) {
|
||||
return Duration.ofSeconds(0);
|
||||
@@ -106,6 +110,9 @@ public class Speed {
|
||||
case IMPERIAL:
|
||||
distance *= UnitConversions.M_TO_MI;
|
||||
break;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
distance *= UnitConversions.M_TO_NAUTICAL_MILE;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
@@ -119,6 +126,8 @@ public class Speed {
|
||||
return toKMH();
|
||||
case IMPERIAL:
|
||||
return toMPH();
|
||||
case NAUTICAL_IMPERIAL:
|
||||
return toKnots();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ public class SpeedFormatter {
|
||||
case IMPERIAL:
|
||||
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
|
||||
break;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
unitId = reportSpeedOrPace ? R.string.unit_knots : R.string.unit_minute_per_nautical_mile;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class TrackPoint {
|
||||
private Double longitude;
|
||||
private Distance horizontalAccuracy;
|
||||
private Distance verticalAccuracy;
|
||||
private Altitude altitude;
|
||||
private Altitude altitude; //TODO use Altitude.WGS84
|
||||
private Speed speed;
|
||||
private Float bearing;
|
||||
private Distance sensorDistance;
|
||||
@@ -56,7 +56,7 @@ public class TrackPoint {
|
||||
|
||||
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
|
||||
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
|
||||
SENSORPOINT(2), //Just BLE sensor data; required to have speed and sensorDistance
|
||||
SENSORPOINT(2), //Just BLE sensor data; may have speed and sensorDistance
|
||||
|
||||
SEGMENT_END_MANUAL(1); //End of a segment
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ public class UnitConversions {
|
||||
|
||||
// Time //TODO Use Duration
|
||||
// multiplication factor to convert seconds to milliseconds
|
||||
@Deprecated
|
||||
public static final long S_TO_MS = 1000;
|
||||
|
||||
// multiplication factor to convert milliseconds to seconds
|
||||
@@ -53,6 +54,9 @@ public class UnitConversions {
|
||||
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
|
||||
// multiplication factor to convert meters to feet
|
||||
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
|
||||
public static final double NAUTICAL_MILE_TO_M = 1852.0;
|
||||
public static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
|
||||
public static final double KM_TO_NAUTICAL_MILE = 1000 * M_TO_NAUTICAL_MILE;
|
||||
// multiplication factor to convert meters per second to kilometers per hour
|
||||
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
|
||||
// multiplication factor to convert miles to km
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package de.dennisguse.opentracks.io.file;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -128,6 +134,23 @@ public enum TrackFileFormat {
|
||||
this.preferenceId = preferenceId;
|
||||
}
|
||||
|
||||
public static Map<String, String> toPreferenceIdLabelMap(final Resources resources, final TrackFileFormat ... trackFileFormats) {
|
||||
Map<String, String> preferenceIdLabelMap = new LinkedHashMap<>(trackFileFormats.length);
|
||||
for (TrackFileFormat trackFileFormat : trackFileFormats) {
|
||||
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
|
||||
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
|
||||
preferenceIdLabelMap.put(trackFileFormat.getPreferenceId(), String.format("%s (%s)", trackFileFormatUpperCase, resources.getString(photoMessageId)));
|
||||
}
|
||||
return preferenceIdLabelMap;
|
||||
}
|
||||
|
||||
public static TrackFileFormat valueOfPreferenceId(final String preferenceId) {
|
||||
return Arrays.stream(values())
|
||||
.filter(trackFileFormat -> trackFileFormat.getPreferenceId().equals(preferenceId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type for each format.
|
||||
*/
|
||||
|
||||
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
|
||||
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
@@ -234,7 +235,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
* @param conflictResolution conflict resolution to be applied if needed.
|
||||
*/
|
||||
private void export(Track track, int conflictResolution) {
|
||||
boolean fileExists = ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles);
|
||||
boolean fileExists = exportFileExists(track);
|
||||
|
||||
if (fileExists && conflictResolution == CONFLICT_NONE) {
|
||||
conflict(track);
|
||||
@@ -250,6 +251,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
export(track, autoConflict);
|
||||
}
|
||||
|
||||
private boolean exportFileExists(Track track) {
|
||||
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
|
||||
return directoryFiles.stream().anyMatch(filename::equals);
|
||||
}
|
||||
|
||||
private void setConflictVisibility(int visibility) {
|
||||
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
|
||||
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
|
||||
@@ -315,7 +321,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
public void onExportSuccess(Track.Id trackId) {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
if (ExportUtils.isExportFileExists(track.getUuid(), trackFileFormat.getExtension(), directoryFiles)) {
|
||||
if (exportFileExists(track)) {
|
||||
trackExportOverwrittenCount++;
|
||||
} else {
|
||||
trackExportSuccessCount++;
|
||||
|
||||
@@ -146,13 +146,10 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
case SEGMENT_START_MANUAL:
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
|
||||
break;
|
||||
case SEGMENT_END_MANUAL:
|
||||
writeCloseSegment();
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_END_MANUAL.name() + " is not supported.");
|
||||
break;
|
||||
|
||||
@@ -68,9 +68,6 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
public static final String EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL = "accuracy_horizontal";
|
||||
public static final String EXTENDED_DATA_TYPE_ACCURACY_VERTICAL = "accuracy_vertical";
|
||||
|
||||
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
|
||||
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
|
||||
|
||||
private static final NumberFormat SENSOR_DATA_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
|
||||
static {
|
||||
@@ -536,7 +533,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
|
||||
printWriter.println("<IconStyle>");
|
||||
printWriter.println("<scale>1.3</scale>");
|
||||
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
|
||||
printWriter.println("<Icon />");
|
||||
printWriter.println("</IconStyle>");
|
||||
printWriter.println("</Style>");
|
||||
}
|
||||
@@ -546,9 +543,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
*/
|
||||
private void writePlacemarkerStyle() {
|
||||
printWriter.println("<Style id=\"" + KMLTrackExporter.MARKER_STYLE + "\"><IconStyle>");
|
||||
printWriter.println("<scale>1.3</scale>");
|
||||
printWriter.println("<Icon><href>" + KMLTrackExporter.MARKER_ICON + "</href></Icon>");
|
||||
printWriter.println("<hotSpot x=\"" + 20 + "\" y=\"" + 2 + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
|
||||
printWriter.println("<Icon />");
|
||||
printWriter.println("</IconStyle></Style>");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,57 @@ package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
|
||||
public class StartRecording extends AbstractAPIActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_NAME = "TRACK_NAME";
|
||||
public static final String EXTRA_TRACK_CATEGORY = "TRACK_CATEGORY";
|
||||
public static final String EXTRA_TRACK_DESCRIPTION = "TRACK_DESCRIPTION";
|
||||
|
||||
public static final String EXTRA_STATS_TARGET_PACKAGE = "STATS_TARGET_PACKAGE";
|
||||
public static final String EXTRA_STATS_TARGET_CLASS = "STATS_TARGET_CLASS";
|
||||
|
||||
private static final String TAG = StartRecording.class.getSimpleName();
|
||||
|
||||
protected void execute(TrackRecordingService service) {
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
if ((trackId != null) && (PreferencesUtils.isPublicAPIDashboardEnabled())) {
|
||||
if (trackId != null) {
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
if (bundle != null) {
|
||||
String targetPackage = bundle.getString("STATS_TARGET_PACKAGE", null);
|
||||
String targetClass = bundle.getString("STATS_TARGET_CLASS", null);
|
||||
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
|
||||
updateTrackMetadata(trackId, bundle);
|
||||
|
||||
if (PreferencesUtils.isPublicAPIDashboardEnabled()) {
|
||||
startDashboardAPI(trackId, bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTrackMetadata(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackUtils.updateTrack(this, track, bundle.getString(EXTRA_TRACK_NAME, null),
|
||||
bundle.getString(EXTRA_TRACK_CATEGORY, null), bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
|
||||
contentProviderUtils);
|
||||
}
|
||||
|
||||
private void startDashboardAPI(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
|
||||
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
|
||||
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
|
||||
if (targetClass != null && targetPackage != null) {
|
||||
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPostExecuteStopService() {
|
||||
return false;
|
||||
|
||||
@@ -5,12 +5,15 @@ import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
|
||||
/**
|
||||
@@ -29,7 +32,7 @@ public class AltitudeSumManager implements SensorEventListener {
|
||||
private Float altitudeGain_m;
|
||||
private Float altitudeLoss_m;
|
||||
|
||||
public void start(Context context) {
|
||||
public void start(Context context, Handler handler) {
|
||||
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
||||
@@ -37,7 +40,7 @@ public class AltitudeSumManager implements SensorEventListener {
|
||||
Log.w(TAG, "No pressure sensor available.");
|
||||
isConnected = false;
|
||||
} else {
|
||||
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
|
||||
isConnected = sensorManager.registerListener(this, pressureSensor, (int) TimeUnit.SECONDS.toMicros(5), handler);
|
||||
}
|
||||
|
||||
lastAcceptedPressureValue_hPa = Float.NaN;
|
||||
|
||||
@@ -136,9 +136,9 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
Log.d(TAG, "Connecting to: " + device);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
bluetoothGatt = device.connectGatt(context, true, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
|
||||
} else {
|
||||
bluetoothGatt = device.connectGatt(context, true, connectCallback);
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback);
|
||||
}
|
||||
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
|
||||
observer.onChanged(sensorData);
|
||||
|
||||
@@ -9,12 +9,10 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
|
||||
public class RecordingStatus {
|
||||
private final Track.Id trackId;
|
||||
private final boolean paused;
|
||||
|
||||
@VisibleForTesting
|
||||
RecordingStatus(Track.Id trackId, boolean paused) {
|
||||
RecordingStatus(Track.Id trackId) {
|
||||
this.trackId = trackId;
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
public Track.Id getTrackId() {
|
||||
@@ -25,24 +23,12 @@ public class RecordingStatus {
|
||||
return trackId != null;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
public boolean isRecordingAndNotPaused() {
|
||||
return isRecording() && !isPaused();
|
||||
}
|
||||
|
||||
static RecordingStatus notRecording() {
|
||||
return new RecordingStatus(null, false);
|
||||
return new RecordingStatus(null);
|
||||
}
|
||||
|
||||
static RecordingStatus record(@NonNull Track.Id trackId) {
|
||||
return new RecordingStatus(trackId, false);
|
||||
}
|
||||
|
||||
RecordingStatus pause() {
|
||||
return new RecordingStatus(getTrackId(), true);
|
||||
return new RecordingStatus(trackId);
|
||||
}
|
||||
|
||||
public RecordingStatus stop() {
|
||||
@@ -54,7 +40,6 @@ public class RecordingStatus {
|
||||
public String toString() {
|
||||
return "RecordingStatus{" +
|
||||
"trackId=" + trackId +
|
||||
", paused=" + paused +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -63,11 +48,11 @@ public class RecordingStatus {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
RecordingStatus that = (RecordingStatus) o;
|
||||
return paused == that.paused && Objects.equals(trackId, that.trackId);
|
||||
return Objects.equals(trackId, that.trackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(trackId, paused);
|
||||
return Objects.hash(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -82,25 +83,23 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
return trackId;
|
||||
}
|
||||
|
||||
//TODO Handle non-existing trackId? Start a new track or exception?
|
||||
void resumeExistingTrack(@NonNull Track.Id resumeTrackId, @NonNull TrackPointCreator trackPointCreator) {
|
||||
/**
|
||||
* @return if the recording could be started.
|
||||
*/
|
||||
boolean resumeExistingTrack(@NonNull Track.Id resumeTrackId, @NonNull TrackPointCreator trackPointCreator) {
|
||||
trackId = resumeTrackId;
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.e(TAG, "Ignore resumeTrack. Track " + trackId.getId() + " does not exists.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
|
||||
onNewTrackPoint(trackPointCreator.createSegmentStartManual());
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void pause(TrackPointCreator trackPointCreator) {
|
||||
insertTrackPoint(trackPointCreator.createSegmentEnd(), true);
|
||||
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
void end(TrackPointCreator trackPointCreator) {
|
||||
@@ -182,8 +181,16 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
}
|
||||
|
||||
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
|
||||
Log.d(TAG, "Ignoring TrackPoint as it has no distance.");
|
||||
return false;
|
||||
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
|
||||
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
|
||||
.isBefore(trackPoint.getTime());
|
||||
if (!shouldStore) {
|
||||
Log.d(TAG, "Ignoring TrackPoint as it has no distance (and sensor data is not new enough).");
|
||||
return false;
|
||||
} else {
|
||||
insertTrackPoint(trackPoint, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Distance distanceToLastStoredTrackPoint;
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -178,23 +177,16 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
public void resumeTrack(Track.Id trackId) {
|
||||
trackPointCreator.reset();
|
||||
trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator);
|
||||
if (!trackRecordingManager.resumeExistingTrack(trackId, trackPointCreator)) {
|
||||
Log.w(TAG, "Cannot resume a non-existing track.");
|
||||
return;
|
||||
}
|
||||
|
||||
updateRecordingStatus(RecordingStatus.record(trackId));
|
||||
|
||||
startRecording();
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public void resumeCurrentTrack() {
|
||||
if (!isRecording() || !isPaused()) {
|
||||
Log.w(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
resumeTrack(recordingStatus.getTrackId());
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
// Update instance variables
|
||||
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
||||
@@ -222,46 +214,18 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to remember the recordingTrackId before setting it to null
|
||||
boolean wasPause = isPaused();
|
||||
Track.Id trackId = recordingStatus.getTrackId();
|
||||
|
||||
// Set recording status
|
||||
updateRecordingStatus(STATUS_DEFAULT);
|
||||
|
||||
if (!wasPause) {
|
||||
trackRecordingManager.end(trackPointCreator);
|
||||
}
|
||||
|
||||
endRecording(true);
|
||||
trackRecordingManager.end(trackPointCreator);
|
||||
endRecording();
|
||||
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public void pauseCurrentTrack() {
|
||||
if (!isRecording() || isPaused()) {
|
||||
Log.w(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set recording status
|
||||
updateRecordingStatus(recordingStatus.pause());
|
||||
|
||||
trackRecordingManager.pause(trackPointCreator);
|
||||
|
||||
endRecording(false);
|
||||
|
||||
notificationManager.updateContent(getString(R.string.generic_paused));
|
||||
}
|
||||
|
||||
private void endRecording(boolean trackStopped) {
|
||||
private void endRecording() {
|
||||
stopUpdateRecordingData();
|
||||
if (!trackStopped) {
|
||||
updateRecordingDataWhileRecording();
|
||||
} else {
|
||||
recordingDataObservable.postValue(NOT_RECORDING);
|
||||
}
|
||||
recordingDataObservable.postValue(NOT_RECORDING);
|
||||
|
||||
voiceAnnouncementManager.stop();
|
||||
|
||||
@@ -289,8 +253,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
@Override
|
||||
public boolean newTrackPoint(TrackPoint trackPoint, Distance thresholdHorizontalAccuracy) {
|
||||
if (!isRecording() || isPaused()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. Not recording or paused.");
|
||||
if (!isRecording()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. Not recording.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -323,7 +287,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
}
|
||||
|
||||
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
|
||||
if (!isRecording() || isPaused()) {
|
||||
if (!isRecording()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -398,9 +362,4 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
public boolean isRecording() {
|
||||
return recordingStatus.isRecording();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean isPaused() {
|
||||
return recordingStatus.isPaused();
|
||||
}
|
||||
}
|
||||
@@ -167,17 +167,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
setTrackRecordingService(null);
|
||||
}
|
||||
|
||||
public void resumeTrack() {
|
||||
if (trackRecordingService != null) {
|
||||
trackRecordingService.resumeCurrentTrack();
|
||||
}
|
||||
}
|
||||
public void pauseTrack() {
|
||||
if (trackRecordingService != null) {
|
||||
trackRecordingService.pauseCurrentTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Marker.Id addMarker(Context context, String name, String category, String description, String photoUrl) {
|
||||
TrackRecordingService trackRecordingService = getServiceIfBound();
|
||||
|
||||
+25
-12
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks.services.announcement;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
@@ -34,8 +35,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||
@@ -56,6 +57,10 @@ public class VoiceAnnouncement {
|
||||
private final AudioManager audioManager;
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private TrackPoint.Id startTrackPointId = null;
|
||||
|
||||
private IntervalStatistics intervalStatistics;
|
||||
private Distance intervalDistance;
|
||||
|
||||
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
||||
@Override
|
||||
@@ -116,6 +121,8 @@ public class VoiceAnnouncement {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
|
||||
intervalStatistics = new IntervalStatistics(intervalDistance);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -132,7 +139,10 @@ public class VoiceAnnouncement {
|
||||
if (ttsFallback == null) {
|
||||
Log.w(TAG, "MediaPlayer for ttsFallback could not be created.");
|
||||
} else {
|
||||
ttsFallback.setAudioStreamType(AUDIO_STREAM);
|
||||
ttsFallback.setAudioAttributes(
|
||||
new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build());
|
||||
ttsFallback.setLooping(false);
|
||||
}
|
||||
}
|
||||
@@ -165,24 +175,27 @@ public class VoiceAnnouncement {
|
||||
return;
|
||||
}
|
||||
|
||||
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
|
||||
if (currentIntervalDistance != intervalDistance) {
|
||||
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
|
||||
intervalDistance = currentIntervalDistance;
|
||||
startTrackPointId = null;
|
||||
}
|
||||
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
|
||||
|
||||
//TODO Do not load all trackpoints for every announcement
|
||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
|
||||
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
|
||||
SensorStatistics sensorStatistics = null;
|
||||
if (track.getId() != null) {
|
||||
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
|
||||
}
|
||||
|
||||
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
|
||||
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track.getCategory()), lastInterval, sensorStatistics);
|
||||
|
||||
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
|
||||
if (announcement.length() > 0) {
|
||||
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
|
||||
+69
-59
@@ -1,7 +1,12 @@
|
||||
package de.dennisguse.opentracks.services.announcement;
|
||||
|
||||
import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRate;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
|
||||
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
@@ -52,68 +57,68 @@ class VoiceAnnouncementUtils {
|
||||
unitDistanceTTS = "mile";
|
||||
unitSpeedTTS = "mile per hour";
|
||||
break;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
perUnitStringId = R.string.voice_per_nautical_mile;
|
||||
distanceId = R.plurals.voiceDistanceNauticalMiles;
|
||||
speedId = R.plurals.voiceSpeedMKnots;
|
||||
unitDistanceTTS = "nautical mile";
|
||||
unitSpeedTTS = "knots";
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
double distanceInUnit = distance.toKM_Miles(unitSystem);
|
||||
|
||||
builder.append(context.getString(R.string.total_distance));
|
||||
long distanceIntegerPart = (long) distanceInUnit;
|
||||
// Extract the decimal part
|
||||
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
|
||||
// Units should always be english singular for TTS.
|
||||
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
|
||||
// Punctuation helps introduce natural pauses in TTS
|
||||
builder.append(".");
|
||||
if (shouldVoiceAnnounceTotalDistance()) {
|
||||
builder.append(context.getString(R.string.total_distance));
|
||||
// Units should always be english singular for TTS.
|
||||
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 2, unitDistanceTTS);
|
||||
// Punctuation helps introduce natural pauses in TTS
|
||||
builder.append(".");
|
||||
}
|
||||
if (distance.isZero()) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Announce time
|
||||
Duration movingTime = trackStatistics.getMovingTime();
|
||||
if (!movingTime.isZero()) {
|
||||
if (shouldVoiceAnnounceMovingTime() && !movingTime.isZero()) {
|
||||
appendDuration(context, builder, movingTime);
|
||||
builder.append(".");
|
||||
}
|
||||
|
||||
if (isReportSpeed) {
|
||||
double speedInUnit = distancePerTime.to(unitSystem);
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.speed));
|
||||
long speedIntegerPart = (long) speedInUnit;
|
||||
// Extract the decimal part
|
||||
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
|
||||
if (currentDistancePerTime != null) {
|
||||
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
||||
double speedInUnit = distancePerTime.to(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.speed));
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
}
|
||||
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
|
||||
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
|
||||
|
||||
if (currentDistancePerTimeInUnit > 0) {
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.lap_speed));
|
||||
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
|
||||
// Extract the decimal part
|
||||
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Duration time = distancePerTime.toPace(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.pace));
|
||||
appendDuration(context, builder, time);
|
||||
builder.append(" ")
|
||||
.append(context.getString(perUnitStringId))
|
||||
.append(".");
|
||||
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
||||
Duration time = distancePerTime.toPace(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.pace));
|
||||
appendDuration(context, builder, time);
|
||||
builder.append(" ")
|
||||
.append(context.getString(perUnitStringId))
|
||||
.append(".");
|
||||
}
|
||||
|
||||
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
|
||||
if (!currentTime.isZero()) {
|
||||
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
|
||||
Duration currentTime = currentDistancePerTime.toPace(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.lap_time));
|
||||
appendDuration(context, builder, currentTime);
|
||||
@@ -123,24 +128,21 @@ class VoiceAnnouncementUtils {
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldVoiceAnnounceHeartRate()) {
|
||||
if (sensorStatistics != null && sensorStatistics.hasHeartRate()) {
|
||||
int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
|
||||
if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) {
|
||||
int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.average_heart_rate));
|
||||
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
|
||||
builder.append(".");
|
||||
}
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.average_heart_rate));
|
||||
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
|
||||
builder.append(".");
|
||||
}
|
||||
if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) {
|
||||
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
|
||||
|
||||
if (currentInterval != null && currentInterval.hasAverageHeartRate()) {
|
||||
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.current_heart_rate));
|
||||
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
|
||||
builder.append(".");
|
||||
}
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.current_heart_rate));
|
||||
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
|
||||
builder.append(".");
|
||||
}
|
||||
|
||||
return builder;
|
||||
@@ -156,26 +158,34 @@ class VoiceAnnouncementUtils {
|
||||
int seconds = (int) (duration.getSeconds() % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, null, "hour");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
|
||||
}
|
||||
if (minutes > 0) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, null, "minute");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
|
||||
}
|
||||
if (seconds > 0 || duration.isZero()) {
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, null, "second");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form]
|
||||
*
|
||||
* @param number The number to speak
|
||||
* @param precision The number of decimal places to announce
|
||||
*/
|
||||
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, long integerPart, @Nullable String fractionalPart, @NonNull String unit) {
|
||||
private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, double number, int precision, @NonNull String unit) {
|
||||
TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder()
|
||||
.setUnit(unit);
|
||||
|
||||
if (fractionalPart == null) {
|
||||
measureBuilder.setNumber(integerPart);
|
||||
if (precision == 0) {
|
||||
measureBuilder.setNumber((long)number);
|
||||
} else {
|
||||
// Round before extracting integral and decimal parts
|
||||
double roundedNumber = Math.round(Math.pow(10, precision) * number) / Math.pow(10.0, precision);
|
||||
long integerPart = (long) roundedNumber;
|
||||
// Extract the decimal part
|
||||
String fractionalPart = String.format("%." + precision + "f", (roundedNumber - integerPart)).substring(2);
|
||||
measureBuilder.setIntegerPart(integerPart)
|
||||
.setFractionalPart(fractionalPart);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
altitudeSumManager = new AltitudeSumManager();
|
||||
|
||||
remoteSensorManager.start();
|
||||
altitudeSumManager.start(context);
|
||||
altitudeSumManager.start(context, handler);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
|
||||
entriesId = R.array.stats_rate_metric_options;
|
||||
break;
|
||||
case IMPERIAL:
|
||||
case NAUTICAL_IMPERIAL:
|
||||
entriesId = R.array.stats_rate_imperial_options;
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
@@ -46,27 +46,15 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
|
||||
private void setExportTrackFileFormatOptions() {
|
||||
final TrackFileFormat[] trackFileFormats = {
|
||||
Map<String, String> options = TrackFileFormat.toPreferenceIdLabelMap(getResources(),
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.GPX,
|
||||
TrackFileFormat.CSV
|
||||
};
|
||||
String[] entries = new String[trackFileFormats.length];
|
||||
String[] entryValues = new String[trackFileFormats.length];
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
TrackFileFormat trackFileFormat = trackFileFormats[i];
|
||||
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
|
||||
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
|
||||
entries[i] = String.format("%s (%s)", trackFileFormatUpperCase, getString(photoMessageId));
|
||||
entryValues[i] = trackFileFormat.getPreferenceId();
|
||||
}
|
||||
|
||||
TrackFileFormat.CSV);
|
||||
ListPreference listPreference = findPreference(getString(R.string.export_trackfileformat_key));
|
||||
listPreference.setEntries(entries);
|
||||
listPreference.setEntryValues(entryValues);
|
||||
listPreference.setEntries(options.values().toArray(new String[0]));
|
||||
listPreference.setEntryValues(options.keySet().toArray(new String[0]));
|
||||
}
|
||||
|
||||
private void setExportDirectorySummary() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
@@ -13,20 +14,10 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private static final String TAG = MainSettingsFragment.class.getSimpleName();
|
||||
|
||||
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private final TrackRecordingServiceConnection.Callback bindServiceCallback =
|
||||
(service, unused) -> service.getRecordingStatusObservable()
|
||||
.observe(MainSettingsFragment.this, this::onRecordingStatusChanged);
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindServiceCallback);
|
||||
|
||||
findPreference(getString(R.string.settings_defaults_key)).setOnPreferenceClickListener(preference -> {
|
||||
((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_defaults_key));
|
||||
return true;
|
||||
@@ -69,20 +60,7 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
trackRecordingServiceConnection.bind(getContext());
|
||||
updatePrefsDependOnRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection.unbind(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayPreferenceDialog(Preference preference) {
|
||||
public void onDisplayPreferenceDialog(@NonNull Preference preference) {
|
||||
DialogFragment dialogFragment = null;
|
||||
|
||||
if (preference instanceof ResetDialogPreference) {
|
||||
@@ -97,24 +75,4 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
super.onDisplayPreferenceDialog(preference);
|
||||
}
|
||||
|
||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||
this.recordingStatus = status;
|
||||
if (isAdded()) {
|
||||
updatePrefsDependOnRecording();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePrefsDependOnRecording() {
|
||||
Preference importExportPreference = findPreference(getString(R.string.settings_import_export_key));
|
||||
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
|
||||
|
||||
boolean isRecording = recordingStatus.isRecording();
|
||||
|
||||
importExportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : getString(R.string.settings_import_export_summary));
|
||||
importExportPreference.setEnabled(!isRecording);
|
||||
|
||||
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : getString(R.string.settings_reset_summary));
|
||||
resetPreference.setEnabled(!isRecording);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
/**
|
||||
@@ -213,7 +214,6 @@ public class PreferencesUtils {
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").equals("")) {
|
||||
|
||||
if (!Locale.US.equals(Locale.getDefault())) {
|
||||
setUnit(UnitSystem.METRIC);
|
||||
} else {
|
||||
@@ -323,7 +323,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalCount(1)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
@@ -342,13 +342,58 @@ public class PreferencesUtils {
|
||||
return getFloat(R.string.voice_speed_rate_key, DEFAULT);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceHeartRate() {
|
||||
return getBoolean(R.string.voice_announce_heart_rate_key, false);
|
||||
public static boolean shouldVoiceAnnounceTotalDistance() {
|
||||
return getBoolean(R.string.voice_announce_total_distance_key, true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceHeartRate(boolean value) {
|
||||
setBoolean(R.string.voice_announce_heart_rate_key, value);
|
||||
public static void setVoiceAnnounceTotalDistance(boolean value) {
|
||||
setBoolean(R.string.voice_announce_total_distance_key, value);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceMovingTime() {
|
||||
return getBoolean(R.string.voice_announce_moving_time_key, true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceMovingTime(boolean value) {
|
||||
setBoolean(R.string.voice_announce_moving_time_key, value);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceAverageSpeedPace() {
|
||||
return getBoolean(R.string.voice_announce_average_speed_pace_key, true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceAverageSpeedPace(boolean value) {
|
||||
setBoolean(R.string.voice_announce_average_speed_pace_key, value);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceLapSpeedPace() {
|
||||
return getBoolean(R.string.voice_announce_lap_speed_pace_key, true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceLapSpeedPace(boolean value) {
|
||||
setBoolean(R.string.voice_announce_lap_speed_pace_key, value);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceLapHeartRate() {
|
||||
return getBoolean(R.string.voice_announce_lap_heart_rate_key, false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceLapHeartRate(boolean value) {
|
||||
setBoolean(R.string.voice_announce_lap_heart_rate_key, value);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceAverageHeartRate() {
|
||||
return getBoolean(R.string.voice_announce_average_heart_rate_key, false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setVoiceAnnounceAverageHeartRate(boolean value) {
|
||||
setBoolean(R.string.voice_announce_average_heart_rate_key, value);
|
||||
}
|
||||
|
||||
public static Distance getRecordingDistanceInterval() {
|
||||
@@ -372,7 +417,7 @@ public class PreferencesUtils {
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.setDecimalCount(0)
|
||||
.setDecimalCount(1)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
@@ -388,6 +433,7 @@ public class PreferencesUtils {
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
case NAUTICAL_IMPERIAL:
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else {
|
||||
@@ -415,7 +461,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalCount(1)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
@@ -432,6 +478,7 @@ public class PreferencesUtils {
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
case NAUTICAL_IMPERIAL:
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else {
|
||||
@@ -487,7 +534,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalCount(1)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
|
||||
@@ -509,6 +556,7 @@ public class PreferencesUtils {
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
case NAUTICAL_IMPERIAL:
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
@@ -560,6 +608,14 @@ public class PreferencesUtils {
|
||||
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
|
||||
}
|
||||
break;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
double valueKnots = Speed.ofKMH(value).toKnots();
|
||||
if (value == idleSpeedDefault) {
|
||||
entries[i] = resources.getString(R.string.value_float_knots_recommended, valueKnots);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_float_knots, valueKnots);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
@@ -761,4 +817,13 @@ public class PreferencesUtils {
|
||||
public static void clear() {
|
||||
sharedPreferences.edit().clear().commit();
|
||||
}
|
||||
|
||||
public static void setShowOnMapFormat(final String showOnMapFormat) {
|
||||
setString(R.string.show_on_map_format_key, showOnMapFormat);
|
||||
}
|
||||
|
||||
public static String getShowOnMapFormat() {
|
||||
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
|
||||
@@ -17,5 +18,7 @@ public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title);
|
||||
findPreference(getString(R.string.publicapi_package_key))
|
||||
.setSummary(BuildConfig.APPLICATION_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import de.dennisguse.opentracks.R;
|
||||
|
||||
public enum UnitSystem {
|
||||
METRIC(R.string.stats_units_metric),
|
||||
IMPERIAL(R.string.stats_units_imperial);
|
||||
IMPERIAL(R.string.stats_units_imperial),
|
||||
NAUTICAL_IMPERIAL(R.string.stats_units_nautical); // Nautical miles with feet
|
||||
|
||||
private final int preferenceIdentifier;
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@ import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
|
||||
public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -28,6 +33,8 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
|
||||
setShowOnMapFormatOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,4 +71,14 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
super.onDisplayPreferenceDialog(preference);
|
||||
}
|
||||
|
||||
private void setShowOnMapFormatOptions() {
|
||||
Map<String, String> options = TrackFileFormat.toPreferenceIdLabelMap(getResources(), IntentDashboardUtils.SHOW_ON_MAP_TRACK_FILE_FORMATS);
|
||||
options.put(IntentDashboardUtils.PREFERENCE_ID_DASHBOARD, getString(R.string.show_on_dashboard));
|
||||
options.put(IntentDashboardUtils.PREFERENCE_ID_ASK, getString(R.string.show_on_map_format_ask));
|
||||
ListPreference listPreference = findPreference(getString(R.string.show_on_map_format_key));
|
||||
listPreference.setEntries(options.values().toArray(new String[0]));
|
||||
listPreference.setEntryValues(options.keySet().toArray(new String[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package de.dennisguse.opentracks.share;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ShareContentProvider;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
|
||||
/**
|
||||
* Used to convert IntentDashboardUtils.startDashboard-requests into {@link TrackFileFormat}.
|
||||
*/
|
||||
public abstract class ShowOnMapProxyActivity extends AppCompatActivity {
|
||||
|
||||
private final TrackFileFormat trackFileFormat;
|
||||
|
||||
protected ShowOnMapProxyActivity(TrackFileFormat trackFileFormat) {
|
||||
this.trackFileFormat = trackFileFormat;
|
||||
}
|
||||
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Set<Track.Id> trackIds = IntentDashboardUtils.extractTrackIdsFromIntent(getIntent());
|
||||
|
||||
showTrackfileFormat(this, trackFileFormat, trackIds);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) as KMZ.
|
||||
*
|
||||
* @param context the context
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
private static void showTrackfileFormat(Context context, TrackFileFormat trackFileFormat, Set<Track.Id> trackIds) {
|
||||
if (trackIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, "SharingTrack", trackFileFormat);
|
||||
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_track_as_trackfileformat, trackFileFormat.getExtension())));
|
||||
}
|
||||
|
||||
public static class KML extends ShowOnMapProxyActivity {
|
||||
public KML() {
|
||||
super(TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GPX extends ShowOnMapProxyActivity {
|
||||
public GPX() {
|
||||
super(TrackFileFormat.GPX);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
|
||||
public class AltitudeRingBuffer extends RingBuffer<Altitude> {
|
||||
|
||||
private Altitude firstAltitude;
|
||||
|
||||
AltitudeRingBuffer(int size) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
AltitudeRingBuffer(RingBuffer<Altitude> toCopy) {
|
||||
super(toCopy);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Number from(Altitude object) {
|
||||
if (firstAltitude == null) {
|
||||
firstAltitude = object;
|
||||
}
|
||||
return object.toM();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Altitude to(double object) {
|
||||
return firstAltitude.replace(object);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This class maintains a ring buffer of doubles.
|
||||
* This buffer is a convenient class for storing a series of doubles and calculating information about them.
|
||||
* This is a FIFO buffer.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
abstract class RingBuffer<T> {
|
||||
|
||||
private final ArrayList<T> buffer;
|
||||
|
||||
// The location that the next write will occur at.
|
||||
private int index;
|
||||
|
||||
// True if the buffer is full
|
||||
private boolean isFull;
|
||||
|
||||
/**
|
||||
* Creates a buffer with a certain size.
|
||||
*
|
||||
* @param size the size
|
||||
*/
|
||||
RingBuffer(int size) {
|
||||
if (size < 1) {
|
||||
throw new IllegalArgumentException("The buffer size must be greater than 1.");
|
||||
}
|
||||
|
||||
buffer = new ArrayList<T>(size);
|
||||
reset();
|
||||
}
|
||||
|
||||
RingBuffer(RingBuffer<T> toCopy) {
|
||||
this.buffer = new ArrayList<>(toCopy.buffer);
|
||||
this.index = toCopy.index;
|
||||
this.isFull = toCopy.isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer.
|
||||
*/
|
||||
public void reset() {
|
||||
index = 0;
|
||||
isFull = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the buffer is full.
|
||||
*/
|
||||
boolean isFull() {
|
||||
return isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average of the buffer.
|
||||
*/
|
||||
public T getAverage() {
|
||||
int numberOfEntries = isFull ? buffer.size() : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return null;
|
||||
}
|
||||
Double sum = null;
|
||||
int numberOfUsedEntries = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
Number value = from(buffer.get(i));
|
||||
if (value != null) {
|
||||
if (sum == null) {
|
||||
sum = 0.0;
|
||||
}
|
||||
sum += value.doubleValue();
|
||||
numberOfUsedEntries++;
|
||||
}
|
||||
}
|
||||
if (sum == null) {
|
||||
return null;
|
||||
} else {
|
||||
return to(sum / numberOfUsedEntries);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract Number from(T object);
|
||||
|
||||
protected abstract T to(double object);
|
||||
|
||||
/**
|
||||
* Adds a double to the buffer.
|
||||
* If the buffer is full the oldest element is overwritten.
|
||||
*
|
||||
* @param value the double to add
|
||||
*/
|
||||
public void setNext(T value) {
|
||||
if (index == buffer.size()) {
|
||||
index = 0;
|
||||
}
|
||||
buffer.add(index, value);
|
||||
index++;
|
||||
if (index == buffer.size()) {
|
||||
isFull = true;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder("Full: ");
|
||||
builder.append(isFull);
|
||||
builder.append("\n");
|
||||
for (int i = 0; i < buffer.size(); i++) {
|
||||
builder.append((i == index) ? "<<" : "[");
|
||||
builder.append(buffer.get(i));
|
||||
builder.append((i == index) ? ">> " : "] ");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.stats;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
|
||||
public class SpeedRingBuffer extends RingBuffer<Speed> {
|
||||
|
||||
SpeedRingBuffer(int size) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
SpeedRingBuffer(RingBuffer<Speed> toCopy) {
|
||||
super(toCopy);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Number from(Speed object) {
|
||||
return object.toMPS();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Speed to(double object) {
|
||||
return Speed.of(object);
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,15 @@ package de.dennisguse.opentracks.stats;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
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.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
|
||||
/**
|
||||
* Updater for {@link TrackStatistics}.
|
||||
@@ -41,30 +40,15 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
*/
|
||||
public class TrackStatisticsUpdater {
|
||||
|
||||
/**
|
||||
* The number of altitude readings to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
private static final int ALTITUDE_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* The number of speed reading to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
private static final int SPEED_SMOOTHING_FACTOR = 25;
|
||||
|
||||
private static final String TAG = TrackStatisticsUpdater.class.getSimpleName();
|
||||
/**
|
||||
* Ignore any acceleration faster than this.
|
||||
* Will ignore any speeds that imply acceleration greater than 2g's
|
||||
* 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
|
||||
*/
|
||||
private static final double MAX_ACCELERATION = 0.02;
|
||||
private static final double SPEED_MAX_ACCELERATION = 2 * 9.81;
|
||||
|
||||
private final TrackStatistics trackStatistics;
|
||||
|
||||
private final AltitudeRingBuffer altitudeBuffer;
|
||||
private final SpeedRingBuffer speedBuffer;
|
||||
private float averageHeartRateBPM;
|
||||
private Duration totalHeartRateDuration = Duration.ZERO;
|
||||
|
||||
@@ -86,8 +70,6 @@ public class TrackStatisticsUpdater {
|
||||
this.trackStatistics = trackStatistics;
|
||||
this.currentSegment = new TrackStatistics();
|
||||
|
||||
altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
|
||||
speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR);
|
||||
resetAverageHeartRate();
|
||||
}
|
||||
|
||||
@@ -95,9 +77,6 @@ public class TrackStatisticsUpdater {
|
||||
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
|
||||
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
|
||||
|
||||
this.altitudeBuffer = new AltitudeRingBuffer(toCopy.altitudeBuffer);
|
||||
this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer);
|
||||
|
||||
this.lastTrackPoint = toCopy.lastTrackPoint;
|
||||
resetAverageHeartRate();
|
||||
}
|
||||
@@ -140,11 +119,7 @@ public class TrackStatisticsUpdater {
|
||||
|
||||
//Update absolute (GPS-based) altitude
|
||||
if (trackPoint.hasAltitude()) {
|
||||
// Update altitude using the smoothed average
|
||||
altitudeBuffer.setNext(trackPoint.getAltitude());
|
||||
Altitude newAverage = altitudeBuffer.getAverage();
|
||||
|
||||
currentSegment.updateAltitudeExtremities(newAverage);
|
||||
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
|
||||
}
|
||||
|
||||
// Update heart rate
|
||||
@@ -178,11 +153,8 @@ public class TrackStatisticsUpdater {
|
||||
|
||||
// Update max speed
|
||||
updateSpeed(trackPoint, lastTrackPoint);
|
||||
} else {
|
||||
speedBuffer.reset();
|
||||
}
|
||||
|
||||
|
||||
if (trackPoint.isSegmentEnd()) {
|
||||
reset(trackPoint);
|
||||
return;
|
||||
@@ -198,8 +170,6 @@ public class TrackStatisticsUpdater {
|
||||
currentSegment.reset(trackPoint.getTime());
|
||||
|
||||
lastTrackPoint = null;
|
||||
altitudeBuffer.reset();
|
||||
speedBuffer.reset();
|
||||
resetAverageHeartRate();
|
||||
}
|
||||
|
||||
@@ -208,30 +178,14 @@ public class TrackStatisticsUpdater {
|
||||
totalHeartRateDuration = Duration.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the smoothed altitude over several readings.
|
||||
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
|
||||
*/
|
||||
public Altitude getSmoothedAltitude() {
|
||||
return altitudeBuffer.getAverage();
|
||||
}
|
||||
|
||||
public Speed getSmoothedSpeed() {
|
||||
return speedBuffer.getAverage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a speed reading while assuming the user is moving.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
||||
if (!trackPoint.isMoving()) {
|
||||
speedBuffer.reset();
|
||||
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
|
||||
speedBuffer.setNext(trackPoint.getSpeed());
|
||||
Speed average = speedBuffer.getAverage();
|
||||
if (average.greaterThan(currentSegment.getMaxSpeed())) {
|
||||
currentSegment.setMaxSpeed(average);
|
||||
if (isValidSpeed(trackPoint, lastTrackPoint)) {
|
||||
Speed currentSpeed = trackPoint.getSpeed();
|
||||
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
|
||||
currentSegment.setMaxSpeed(currentSpeed);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
|
||||
@@ -239,30 +193,13 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
|
||||
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
|
||||
if (trackPoint.getSpeed().isZero()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
|
||||
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
Speed maxAcceleration = Speed.of(MAX_ACCELERATION * timeDifference.toMillis());
|
||||
{
|
||||
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
|
||||
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
|
||||
if (speedDifference.greaterThan(maxAcceleration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Speed maxSpeedDifference = Speed.of(Distance.of(SPEED_MAX_ACCELERATION), Duration.ofMillis(1000))
|
||||
.mul(timeDifference.toMillis() / UnitConversions.S_TO_MS);
|
||||
|
||||
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
|
||||
if (speedBuffer.isFull()) {
|
||||
Speed average = speedBuffer.getAverage();
|
||||
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
|
||||
|
||||
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
|
||||
}
|
||||
|
||||
return true;
|
||||
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
|
||||
return speedDifference.lessThan(maxSpeedDifference);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -49,8 +49,7 @@ public class IntervalStatistics {
|
||||
interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint);
|
||||
|
||||
double adjustFactor = distanceInterval.dividedBy(interval.distance);
|
||||
Interval adjustedInterval = new Interval(interval);
|
||||
adjustedInterval.adjust(adjustFactor);
|
||||
Interval adjustedInterval = new Interval(interval, adjustFactor);
|
||||
|
||||
intervalList.set(intervalList.size() - 1, adjustedInterval);
|
||||
|
||||
@@ -116,7 +115,6 @@ public class IntervalStatistics {
|
||||
public Interval(Interval i, double adjustFactor) {
|
||||
distance = i.distance.multipliedBy(adjustFactor);
|
||||
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
|
||||
time = i.time;
|
||||
gain_m = i.gain_m;
|
||||
loss_m = i.loss_m;
|
||||
avgHeartRate = i.avgHeartRate;
|
||||
@@ -130,11 +128,6 @@ public class IntervalStatistics {
|
||||
avgHeartRate = i.avgHeartRate;
|
||||
}
|
||||
|
||||
private void adjust(double adjustFactor) {
|
||||
distance = distance.multipliedBy(adjustFactor);
|
||||
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
|
||||
}
|
||||
|
||||
public Distance getDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
@@ -107,17 +107,17 @@ public class IntervalsFragment extends Fragment {
|
||||
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||
viewBinding.intervalList.setAdapter(adapter);
|
||||
|
||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
IntervalStatisticsModel.IntervalOption option = getItem(position);
|
||||
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
|
||||
v.setText(stringValue);
|
||||
|
||||
@@ -21,8 +21,10 @@ import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.Menu;
|
||||
@@ -55,6 +57,8 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String CAMERA_PHOTO_URI_KEY = "camera_photo_uri_key";
|
||||
|
||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||
private Track.Id trackId;
|
||||
private Marker marker;
|
||||
@@ -86,6 +90,10 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
cameraPhotoUri = Uri.parse(savedInstanceState.getString(CAMERA_PHOTO_URI_KEY, ""));
|
||||
}
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
|
||||
// Setup UI elements
|
||||
@@ -165,6 +173,14 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
takePictureFromCamera = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (cameraPhotoUri != null) {
|
||||
outState.putString(CAMERA_PHOTO_URI_KEY, cameraPhotoUri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.marker_edit, menu);
|
||||
@@ -232,7 +248,13 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
}
|
||||
|
||||
private void createMarkerWithGalleryImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
Intent intent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 1);
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
}
|
||||
intent.setType("image/*");
|
||||
takePictureFromGallery.launch(intent);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()) && !recordingStatus.isPaused());
|
||||
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()));
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +85,6 @@ public class ExportUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isExportFileExists(UUID uuuid, String trackFileFormatExtension, List<String> filesName) {
|
||||
String word = uuuid.toString().substring(0, 8) + ".*\\." + trackFileFormatExtension;
|
||||
Pattern pattern = Pattern.compile(word);
|
||||
return filesName.stream().anyMatch(w -> pattern.matcher(w).matches());
|
||||
}
|
||||
|
||||
public static List<String> getAllFiles(Context context, Uri directoryUri) {
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
@@ -1,35 +1,52 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.ShareContentProvider;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Create an {@link Intent} to request showing a Dashboard.
|
||||
* Create an {@link Intent} to request showing tracks on a Map or a Dashboard.
|
||||
* The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update).
|
||||
*/
|
||||
public class IntentDashboardUtils {
|
||||
|
||||
private static final String TAG = IntentDashboardUtils.class.getSimpleName();
|
||||
|
||||
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
|
||||
|
||||
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
|
||||
|
||||
public static final TrackFileFormat[] SHOW_ON_MAP_TRACK_FILE_FORMATS = new TrackFileFormat[] {
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
||||
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.GPX};
|
||||
|
||||
public static final String PREFERENCE_ID_DASHBOARD = ACTION_DASHBOARD;
|
||||
public static final String PREFERENCE_ID_ASK = "ASK";
|
||||
|
||||
/**
|
||||
* Assume "v1" if not present.
|
||||
*/
|
||||
@@ -49,26 +66,80 @@ public class IntentDashboardUtils {
|
||||
private static final int TRACK_URI_INDEX = 0;
|
||||
private static final int TRACKPOINTS_URI_INDEX = 1;
|
||||
private static final int MARKERS_URI_INDEX = 2;
|
||||
private static final int NONE_SELECTED = -1;
|
||||
|
||||
private IntentDashboardUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) as resource URIs.
|
||||
* Shows an AlertDialog with different format option if none is defined as preference.
|
||||
*
|
||||
* @param context the context
|
||||
* @param isRecording are we currently recording?
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
public static void startDashboard(Context context, boolean isRecording, Track.Id... trackIds) {
|
||||
startDashboard(context, isRecording, null, null, trackIds);
|
||||
public static void showTrackOnMap(Context context, boolean isRecording, Track.Id... trackIds) {
|
||||
Map<String, String> options = TrackFileFormat.toPreferenceIdLabelMap(context.getResources(), IntentDashboardUtils.SHOW_ON_MAP_TRACK_FILE_FORMATS);
|
||||
options.put(IntentDashboardUtils.PREFERENCE_ID_DASHBOARD, context.getString(R.string.show_on_dashboard));
|
||||
final String[] optionLabels = options.values().toArray(new String[0]);
|
||||
final String[] optionValues = options.keySet().toArray(new String[0]);
|
||||
final AtomicInteger checkedItem = new AtomicInteger(NONE_SELECTED);
|
||||
String preferenceValue = PreferencesUtils.getShowOnMapFormat();
|
||||
for (int i = 0; i < optionValues.length; i++) {
|
||||
if (optionValues[i].equals(preferenceValue)) {
|
||||
checkedItem.set(i);
|
||||
}
|
||||
}
|
||||
if (checkedItem.get() == NONE_SELECTED) {
|
||||
checkedItem.set(0); // set first option as default
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.select_show_on_map_behavior);
|
||||
builder.setSingleChoiceItems(optionLabels, checkedItem.get(), (dialog, which) -> checkedItem.set(which));
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onFormatSelected(context, isRecording, optionValues[checkedItem.get()], trackIds, false));
|
||||
builder.setNeutralButton(R.string.always, ((dialog, which) -> onFormatSelected(context, isRecording, optionValues[checkedItem.get()], trackIds, true)));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
} else {
|
||||
onFormatSelected(context, isRecording, preferenceValue, trackIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A format was selected, remember if <code>always</code> is true and start the necessary action
|
||||
*
|
||||
* @param context the context
|
||||
* @param isRecording are we currently recording?
|
||||
* @param selectedValue the chosen format
|
||||
* @param trackIds the track ids
|
||||
* @param always set the selectedValue as default preference
|
||||
*/
|
||||
private static void onFormatSelected(final Context context, final boolean isRecording, final String selectedValue, final Track.Id[] trackIds, final boolean always) {
|
||||
if (always) {
|
||||
PreferencesUtils.setShowOnMapFormat(selectedValue);
|
||||
}
|
||||
TrackFileFormat trackFileFormat = TrackFileFormat.valueOfPreferenceId(selectedValue);
|
||||
if (trackFileFormat != null) {
|
||||
showTrackOnMapWithFileFormat(context, trackFileFormat, Set.of(trackIds));
|
||||
} else {
|
||||
startDashboard(context, isRecording, null, null, trackIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) as resource URIs.
|
||||
* By providing a targetPackage and targetClass an explicit intent can be sent,
|
||||
* thus bypassing the need for the user to select an app.
|
||||
*
|
||||
* @param context the context
|
||||
* @param isRecording are we currently recording?
|
||||
* @param targetPackage the target package
|
||||
* @param targetClass the target class
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
public static void startDashboard(Context context, boolean isRecording, String targetPackage, String targetClass, Track.Id... trackIds) {
|
||||
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
|
||||
if (trackIds.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -98,26 +169,42 @@ public class IntentDashboardUtils {
|
||||
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
|
||||
intent.setClipData(clipData);
|
||||
|
||||
if ((targetPackage != null) && (targetClass != null)) {
|
||||
if (targetPackage != null && targetClass != null) {
|
||||
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
intent.setClassName(targetPackage, targetClass);
|
||||
} else {
|
||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "Dashboard not installed; cannot start it.");
|
||||
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<Track.Id> extractTrackIdsFromIntent(@NonNull Intent intent) {
|
||||
final ArrayList<Uri> uris = intent.getParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD);
|
||||
final Uri tracksUri = uris.get(TRACK_URI_INDEX);
|
||||
|
||||
String[] trackIdsString = ContentProviderUtils.parseTrackIdsFromUri(tracksUri);
|
||||
Set<Track.Id> trackIds = new HashSet<>(trackIdsString.length);
|
||||
for (String s : trackIdsString) {
|
||||
trackIds.add(new Track.Id(Long.parseLong(s)));
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) to support specific trackFileFormat.
|
||||
*
|
||||
* @param context the context
|
||||
* @param trackFileFormat the track file format
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
private static void showTrackOnMapWithFileFormat(Context context, TrackFileFormat trackFileFormat, Set<Track.Id> trackIds) {
|
||||
if (trackIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return trackIds;
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, "SharingTrack", trackFileFormat);
|
||||
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_track_as_trackfileformat, trackFileFormat.getExtension())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@@ -6,18 +6,18 @@
|
||||
<path
|
||||
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
|
||||
android:strokeWidth="5.03185"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ff6d00"/>
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@color/opentracks_secondary_color"/>
|
||||
<path
|
||||
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
|
||||
android:strokeWidth="6.91728"
|
||||
android:fillColor="#ff6d00"
|
||||
android:strokeColor="#00000000"/>
|
||||
android:strokeWidth="1.0"
|
||||
android:fillColor="@color/opentracks_secondary_color"
|
||||
android:strokeColor="@color/opentracks_secondary_color"/>
|
||||
<path
|
||||
android:pathData="M38.62,63.866 L52.899,78.144 86.38,44.664"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="6.2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:fillColor="@color/opentracks_secondary_color"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
|
||||
android:strokeWidth="5.03185225"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#a31a1f" />
|
||||
<path
|
||||
android:fillAlpha="0.87843137"
|
||||
android:fillColor="#efefef"
|
||||
android:pathData="M54,54.059h5v17.941h-5z"
|
||||
android:strokeWidth="4.82273912"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillAlpha="0.87843137"
|
||||
android:fillColor="#efefef"
|
||||
android:pathData="M66,54h5v17.941h-5z"
|
||||
android:strokeWidth="4.82273865"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,27 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
|
||||
android:strokeWidth="5.03185225"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#a31a1f" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#9a191d"
|
||||
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
|
||||
android:strokeWidth="6.9172821"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M62.5,61.5m-9.189,0a9.189,9.189 0,1 1,18.378 0a9.189,9.189 0,1 1,-18.378 0"
|
||||
android:strokeWidth="1.62162137"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#7e3b3d" />
|
||||
</vector>
|
||||
@@ -1,34 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
|
||||
android:strokeWidth="5.0319"
|
||||
android:strokeColor="#a31a1f" />
|
||||
<path android:pathData="M62.5,62.5m-0,-50a50,50 0,1 1,-0 100a50,50 0,1 1,-0 -100">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="62.5"
|
||||
android:endY="112.5"
|
||||
android:startX="62.5"
|
||||
android:startY="12.5"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FF721212"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFA21A1F"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M62.5,61.5m-9.189,0a9.189,9.189 0,1 1,18.378 0a9.189,9.189 0,1 1,-18.378 0"
|
||||
android:strokeWidth="1.6216"
|
||||
android:strokeColor="#7e3b3d" />
|
||||
</vector>
|
||||
@@ -6,17 +6,17 @@
|
||||
<path
|
||||
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
|
||||
android:strokeWidth="5.03185"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ff6d00"/>
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@color/opentracks_secondary_color"/>
|
||||
<path
|
||||
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
|
||||
android:strokeWidth="6.91728"
|
||||
android:fillColor="#ff6d00"
|
||||
android:strokeColor="#00000000"/>
|
||||
android:fillColor="@color/opentracks_secondary_color"
|
||||
android:strokeColor="@android:color/transparent"/>
|
||||
<path
|
||||
android:pathData="M87.6,62.5 L67.991,73.822 48.381,85.143l-0,-22.643 0,-22.643 19.61,11.322z"
|
||||
android:strokeWidth="6.67087"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="@android:color/transparent"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2.516,63.008a60.239,59.73 134.879,1 0,119.968 -1.017a60.239,59.73 134.879,1 0,-119.968 1.017z"
|
||||
android:strokeWidth="5.03194237"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#555555" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#484848"
|
||||
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
|
||||
android:strokeWidth="6.9172821"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
|
||||
android:strokeWidth="1.04023623"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#414141" />
|
||||
<path
|
||||
android:name="fade_out_circle"
|
||||
android:fillAlpha="0.0"
|
||||
android:fillColor="#a31a1f"
|
||||
android:pathData="M62.5,62.5m-0,-62a50,50 0,1 1,-0 123a50,50 0,1 1,-0 -123" />
|
||||
</vector>
|
||||
@@ -1,28 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2.516,63.008a60.239,59.73 134.879,1 0,119.968 -1.017a60.239,59.73 134.879,1 0,-119.968 1.017z"
|
||||
android:strokeWidth="5.03194237"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#2a2a2a" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#2a2a2a"
|
||||
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
|
||||
android:strokeWidth="6.9172821"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000" />
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#555555"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M54,56h15v15h-15z"
|
||||
android:strokeWidth="1.56054175"
|
||||
android:strokeAlpha="0.4"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,34 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="82dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="125"
|
||||
android:viewportHeight="125">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M2.516,63.009a60.239,59.73 134.856,1 0,119.967 -1.017a60.239,59.73 134.856,1 0,-119.967 1.017z"
|
||||
android:strokeWidth="5.0319"
|
||||
android:strokeColor="#555" />
|
||||
<path android:pathData="M62.5,62.5m-0,-50a50,50 0,1 1,-0 100a50,50 0,1 1,-0 -100">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="62.5"
|
||||
android:endY="112.5"
|
||||
android:startX="62.5"
|
||||
android:startY="12.5"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FF404040"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FF555555"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#dbdbdb"
|
||||
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
|
||||
android:strokeWidth="1.0402"
|
||||
android:strokeColor="#414141" />
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/loading_indeterminate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_text"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/loading_indicator"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="Text" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/loading_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -74,8 +74,8 @@ limitations under the License.
|
||||
style="?attr/textAppearanceHeadline6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="5"
|
||||
android:gravity="right"
|
||||
android:layout_weight="10"
|
||||
android:gravity="end"
|
||||
android:text="@string/track_list_empty_message" />
|
||||
|
||||
<View
|
||||
@@ -101,6 +101,7 @@ limitations under the License.
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_record_24"
|
||||
android:contentDescription="@string/image_record"
|
||||
app:layout_anchor="@id/bottom_app_bar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -45,6 +45,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_baseline_record_24"
|
||||
android:contentDescription="@string/image_stop"
|
||||
app:layout_anchor="@id/bottom_app_bar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -2,114 +2,224 @@
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/fields"
|
||||
layout="@layout/track_edit_fields" />
|
||||
android:id="@+id/loading_layout"
|
||||
layout="@layout/loading_indeterminate"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout style="@style/BottomButtonBar">
|
||||
<LinearLayout
|
||||
android:id="@+id/content_linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
<ScrollView style="@style/ScrollView.WithBottomButtons">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
android:padding="8dp">
|
||||
|
||||
<!-- Time -->
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceHeadline2"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="04:54:36" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textInputOutlinedStyle"
|
||||
android:hint="@string/generic_name"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<!-- Distance -->
|
||||
<TextView
|
||||
android:id="@+id/distance"
|
||||
style="?attr/textAppearanceHeadline3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/distance_unit"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/time"
|
||||
tools:layout_editor_absoluteX="52dp"
|
||||
tools:text="130" />
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/track_edit_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences"
|
||||
android:selectAllOnFocus="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/distance_unit"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryUnit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/distance"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toEndOf="@+id/distance"
|
||||
tools:text="km" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Average moving speed -->
|
||||
<TextView
|
||||
android:id="@+id/speed"
|
||||
style="?attr/textAppearanceHeadline3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/speed_unit"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/time"
|
||||
tools:text="26,5" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/speed_unit"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryUnit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/speed"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/speed"
|
||||
tools:text="km/h" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textInputOutlinedExposedDropdownMenuStyle"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/stats_summary_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="time,distance,speed" />
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/track_edit_activity_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:hint="@string/track_edit_activity_type_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:selectAllOnFocus="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/resume_button"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginTop="48dp"
|
||||
app:srcCompat="@drawable/ic_button_resume"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_summary_barrier"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/finish_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/finish_button"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
app:srcCompat="@drawable/ic_button_done"
|
||||
app:layout_constraintTop_toTopOf="@id/resume_button"
|
||||
app:layout_constraintStart_toEndOf="@id/resume_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/resume_button" />
|
||||
<ImageView
|
||||
android:id="@+id/track_edit_activity_type_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
tools:src="@drawable/ic_activity_bike_24dp"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textInputOutlinedStyle"
|
||||
android:hint="@string/generic_description"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/track_edit_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences"
|
||||
android:selectAllOnFocus="true" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<!-- Time -->
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="?attr/textAppearanceHeadline2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="04:54:36" />
|
||||
|
||||
<!-- Distance -->
|
||||
<TextView
|
||||
android:id="@+id/distance"
|
||||
style="?attr/textAppearanceHeadline3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/distance_unit"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/time"
|
||||
tools:layout_editor_absoluteX="52dp"
|
||||
tools:text="130" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/distance_unit"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryUnit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/distance"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toEndOf="@+id/distance"
|
||||
tools:text="km" />
|
||||
|
||||
<!-- Average moving speed -->
|
||||
<TextView
|
||||
android:id="@+id/speed"
|
||||
style="?attr/textAppearanceHeadline3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/speed_unit"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/time"
|
||||
tools:text="26,5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/speed_unit"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryUnit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/speed"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/speed"
|
||||
tools:text="km/h" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout style="@style/BottomButtonBar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/buttons_guideline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/discard_button"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="52dp"
|
||||
android:contentDescription="@string/image_discard"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/resume_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_delete_forever_24dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/resume_button"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:contentDescription="@string/image_record"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/buttons_guideline"
|
||||
app:layout_constraintStart_toEndOf="@id/buttons_guideline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_button_resume" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/finish_button"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:contentDescription="@string/image_stop"
|
||||
android:layout_marginStart="36dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/resume_button"
|
||||
app:layout_constraintStart_toEndOf="@id/resume_button"
|
||||
app:layout_constraintTop_toTopOf="@id/resume_button"
|
||||
app:srcCompat="@drawable/ic_button_done" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -117,7 +117,6 @@ limitations under the License.
|
||||
<string name="generic_description_line">الوصف: %1$s</string>
|
||||
<string name="generic_name">الاسم</string>
|
||||
<string name="generic_name_line">الاسم: %1$s</string>
|
||||
<string name="generic_paused">متوقف مؤقتًا</string>
|
||||
<string name="generic_recording">جارٍ التسجيل…</string>
|
||||
<string name="generic_save">حفظ</string>
|
||||
<string name="generic_tracks">المسارات</string>
|
||||
@@ -129,7 +128,6 @@ limitations under the License.
|
||||
<string name="image_arrow">سهم</string>
|
||||
<string name="image_marker">علامة</string>
|
||||
<string name="image_marker_count">عدد العلامات</string>
|
||||
<string name="image_pause">إيقاف مؤقت</string>
|
||||
<string name="image_photo_marker">علامة الصورة</string>
|
||||
<string name="image_record">تسجيل</string>
|
||||
<string name="image_stop">إيقاف</string>
|
||||
@@ -148,7 +146,6 @@ limitations under the License.
|
||||
<!-- Marker Detail -->
|
||||
<!-- Marker Edit -->
|
||||
<string name="marker_edit_marker_type">نوع المُحدد (على سبيل المثال شلال)</string>
|
||||
<string name="marker_edit_marker_type_hint">نوع المُحدد</string>
|
||||
<!-- Marker List -->
|
||||
<string name="marker_list_empty_message">ليس لديك أية علامات</string>
|
||||
<!-- Menu -->
|
||||
@@ -172,21 +169,14 @@ limitations under the License.
|
||||
<string name="recording_service">خدمة التسجيل</string>
|
||||
<!-- Search -->
|
||||
<!-- Sensor State -->
|
||||
<string name="sensor_state_battery">مستوى شحن البطارية</string>
|
||||
<string name="sensor_state_cadence">الإيقاع</string>
|
||||
<string name="sensor_state_cadence_value">%1$d دورة في الدقيقة</string>
|
||||
<string name="sensor_state_heart_rate">معدل ضربات القلب</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d نبضة في الدقيقة</string>
|
||||
<string name="sensor_state_power">الطاقة</string>
|
||||
<string name="sensor_state_power_value">%1$d واط</string>
|
||||
<string name="settings_not_while_recording">غير متاح أثناء تسجيل مسار</string>
|
||||
<string name="settings_show">إظهار</string>
|
||||
<!-- Settings Advanced -->
|
||||
<string name="settings_advanced">خيارات متقدمة</string>
|
||||
<!-- Settings Chart -->
|
||||
<string name="settings_chart_by_distance">بحسب المسافة</string>
|
||||
<string name="settings_chart_by_time">بحسب الوقت</string>
|
||||
<string name="settings_recording">التسجيل</string>
|
||||
<string name="settings_recording_default_activity_title">نوع النشاط الافتراضي</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s بين المواقع المسجلة</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">يتم إنشاء جزء جديد إذا كانت المسافة بين موقعين أكبر من %1$s</string>
|
||||
@@ -200,7 +190,6 @@ limitations under the License.
|
||||
<string name="settings_recording_track_name_number_option">عدد</string>
|
||||
<string name="settings_recording_track_name_title">اسم المسار الافتراضي</string>
|
||||
<!-- Settings Reset -->
|
||||
<string name="settings_reset">إعادة تعيين</string>
|
||||
<string name="settings_reset_confirm_message">ستتم إعادة جميع الإعدادات إلى القيم الافتراضية.</string>
|
||||
<string name="settings_reset_confirm_title">إعادة التعيين الآن؟</string>
|
||||
<string name="settings_reset_done">لقد تمت إعادة جميع الإعدادات إلى القيم الافتراضية</string>
|
||||
@@ -311,25 +300,6 @@ limitations under the License.
|
||||
<item quantity="many">%1$.1f ميلاً في الساعة</item>
|
||||
<item quantity="other">%1$.1f من الأميال في الساعة</item>
|
||||
</plurals>
|
||||
<string name="voice_pace_per_kilometer">%1$s لكل كيلومتر</string>
|
||||
<string name="voice_pace_per_mile">%1$s لكل ميل</string>
|
||||
<string name="voice_template">%1$s في %2$s بسرعة %3$s</string>
|
||||
<plurals name="voiceTotalDistanceKilometers">
|
||||
<item quantity="zero">المسافة الإجمالية 0 كيلومتر</item>
|
||||
<item quantity="one">المسافة الإجمالية كيلومتر واحد</item>
|
||||
<item quantity="two">المسافة الإجمالية 2 كيلومتر</item>
|
||||
<item quantity="few">المسافة الإجمالية %1$.2f كيلومتر</item>
|
||||
<item quantity="many">المسافة الإجمالية %1$.2f كيلومتر</item>
|
||||
<item quantity="other">المسافة الإجمالية %1$.2f كيلومتر</item>
|
||||
</plurals>
|
||||
<plurals name="voiceTotalDistanceMiles">
|
||||
<item quantity="zero">المسافة الإجمالية 2 ميل</item>
|
||||
<item quantity="one">إجمالي المسافة ميل واحد</item>
|
||||
<item quantity="two">المسافة الإجمالية 0 ميل</item>
|
||||
<item quantity="few">المسافة الإجمالية %1$.2f أميال</item>
|
||||
<item quantity="many">مجموع المسافة %1$.2f ميلاً</item>
|
||||
<item quantity="other">المسافة الإجمالية %1$.2f من الأميال</item>
|
||||
</plurals>
|
||||
<!-- Waypoint Type -->
|
||||
<string name="waypoint_type_atm">أجهزة الصراف الآلي</string>
|
||||
<string name="waypoint_type_bank">مصرف</string>
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
<string name="generic_description_line">Description: %1$s</string>
|
||||
<string name="generic_name">Name</string>
|
||||
<string name="generic_name_line">Name: %1$s</string>
|
||||
<string name="generic_paused">Paused</string>
|
||||
<string name="generic_recording">Recording…</string>
|
||||
<string name="generic_save">Save</string>
|
||||
<string name="generic_tracks">Tracks</string>
|
||||
@@ -107,7 +106,6 @@
|
||||
<string name="image_arrow">Arrow</string>
|
||||
<string name="image_marker">Marker</string>
|
||||
<string name="image_marker_count">Marker count</string>
|
||||
<string name="image_pause">Pause</string>
|
||||
<string name="image_photo_marker">Photo marker</string>
|
||||
<string name="image_record">Record</string>
|
||||
<string name="image_stop">Stop</string>
|
||||
@@ -122,7 +120,6 @@
|
||||
<string name="marker_name_format">Marker %1$d</string>
|
||||
<string name="marker_title">%1$d of %2$d</string>
|
||||
<string name="marker_edit_marker_type">Marker type (e.g. waterfall)</string>
|
||||
<string name="marker_edit_marker_type_hint">Marker type</string>
|
||||
<string name="marker_list_empty_message">You don\'t have any markers</string>
|
||||
<string name="menu_aggregated_statistics">Aggregated stats</string>
|
||||
<string name="menu_delete">Delete</string>
|
||||
@@ -141,19 +138,12 @@
|
||||
<string name="menu_start_gps">Start GPS</string>
|
||||
<string name="menu_stop_gps">Stop GPS</string>
|
||||
<string name="recording_service">Recording service</string>
|
||||
<string name="sensor_state_battery">Battery level</string>
|
||||
<string name="sensor_state_cadence">Cadence</string>
|
||||
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
||||
<string name="sensor_state_heart_rate">Heart rate</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
|
||||
<string name="sensor_state_power">Power</string>
|
||||
<string name="sensor_state_power_value">%1$d W</string>
|
||||
<string name="settings_not_while_recording">Not available while recording a track</string>
|
||||
<string name="settings_show">Show</string>
|
||||
<string name="settings_advanced">Advanced</string>
|
||||
<string name="settings_chart_by_distance">By distance</string>
|
||||
<string name="settings_chart_by_time">By time</string>
|
||||
<string name="settings_recording">Recording</string>
|
||||
<string name="settings_recording_default_activity_title">Default activity type</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s between recorded locations</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">A new segment is created if the distance between two locations is greater than %1$s</string>
|
||||
@@ -166,7 +156,6 @@
|
||||
<string name="settings_recording_track_name_date_local_option">Date (local)</string>
|
||||
<string name="settings_recording_track_name_number_option">Number</string>
|
||||
<string name="settings_recording_track_name_title">Default track name</string>
|
||||
<string name="settings_reset">Reset</string>
|
||||
<string name="settings_reset_confirm_message">All settings will be reverted to default values.</string>
|
||||
<string name="settings_reset_confirm_title">Reset now?</string>
|
||||
<string name="settings_reset_done">All settings have been reverted to default values</string>
|
||||
@@ -243,9 +232,6 @@
|
||||
<item quantity="one">1 mile per hour</item>
|
||||
<item quantity="other">%1$.1f miles per hour</item>
|
||||
</plurals>
|
||||
<string name="voice_pace_per_kilometer">%1$s per kilometer</string>
|
||||
<string name="voice_pace_per_mile">%1$s per mile</string>
|
||||
<string name="voice_template">%1$s in %2$s at %3$s</string>
|
||||
<string name="waypoint_type_atm">Cashpoint</string>
|
||||
<string name="waypoint_type_bank">bank</string>
|
||||
<string name="waypoint_type_bar">bar/pub</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user