Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e28c385e52 | |||
| 8d4418a584 | |||
| 85c7314fa8 | |||
| 06a05c82bc | |||
| 4d08729284 | |||
| 28bbf14efd | |||
| 5686322aa6 | |||
| a6c06efb62 | |||
| 80a5149207 | |||
| 5b31213770 | |||
| 19cbbb90f9 | |||
| 39e4a697c0 | |||
| 196de0c3d4 | |||
| 8aa02d009d | |||
| 9a89295b1f | |||
| 4c3c93511f | |||
| bfd6517a61 | |||
| 6334dcea04 | |||
| 5b2d9a5773 | |||
| f1ee0ea035 | |||
| 57afedb3a8 | |||
| 4e33ad4007 | |||
| eada954299 | |||
| 288fe27a57 | |||
| 54e8fd3ee3 | |||
| 68c8839ebb | |||
| 12e764c44a | |||
| 50eff0d7e8 | |||
| ee96226faf | |||
| f5ec91f0eb | |||
| b0575c0f5b | |||
| ccfcd3b306 | |||
| 7929d219ac | |||
| d6f4d3f599 | |||
| 4171d9cc4c | |||
| 9366c031b3 | |||
| 3454fe0cfa | |||
| 3ad2d725be | |||
| d3400dbdfb | |||
| dc4d2ecd89 | |||
| a1feb7d0d5 | |||
| 5cad99b237 | |||
| fb8a4de79b | |||
| b1b0a45f53 | |||
| e580a03115 | |||
| 591597e4e8 | |||
| 1d7820469c | |||
| aa34f5d55b | |||
| e71ab28701 | |||
| ae001c73dd | |||
| 56ff7827df | |||
| 2cdf122e8c | |||
| 495ef2fc6f | |||
| ff561c1b25 | |||
| 7106f64a3c | |||
| 8420f9febb | |||
| 0744ba790f | |||
| 9a7f24bddc | |||
| ef47c86e7c | |||
| 93f203bcf1 | |||
| 6b0ec72b9b | |||
| d46cea6150 | |||
| 0e3ffa58f0 | |||
| c00d769188 | |||
| 00489a121a | |||
| d7f339c6f8 | |||
| da28ea3e95 | |||
| 9872658f81 | |||
| b6b155f19c | |||
| 067f31ac21 | |||
| 3f0ce27870 | |||
| 438f8fe273 | |||
| 392c02a56f | |||
| 6dca3bea58 | |||
| 13034c8bb3 | |||
| 1175ce270e | |||
| 02cad98a4f | |||
| d6b38a390e | |||
| 98deed8ef7 | |||
| 9983168cb9 | |||
| 7b5f29db09 | |||
| 49efd04326 | |||
| 3159c62eb2 | |||
| 0e8811d8c9 | |||
| 4126e36f1c | |||
| b7e2df9f2f | |||
| 055093772d | |||
| 7f8b49814b | |||
| 5cc0e06fbb | |||
| 311a2550e5 | |||
| 4311b29833 | |||
| 86121ac27a | |||
| ed6daeb48b | |||
| af72960cf6 | |||
| 99767d0c0b | |||
| 1fc0a552c7 | |||
| 18dd1de1f9 |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ jobs:
|
|||||||
target: [default]
|
target: [default]
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.4
|
||||||
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
uses: reactivecircus/android-emulator-runner@v2.17.0
|
||||||
with:
|
with:
|
||||||
api-level: ${{ matrix.api-level }}
|
api-level: ${{ matrix.api-level }}
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<component name="RunConfigurationProducerService">
|
<component name="RunConfigurationProducerService">
|
||||||
<option name="ignoredProducers">
|
<option name="ignoredProducers">
|
||||||
<set>
|
<set>
|
||||||
|
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||||
|
|||||||
@@ -49,39 +49,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Features:
|
|
||||||
* __Tracking:__ track your sport and outdoor activities
|
|
||||||
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
|
|
||||||
* __Bluetooth LE sensors:__
|
|
||||||
* heart rate
|
|
||||||
* cycling: speed and cadence
|
|
||||||
* cycling: power meter
|
|
||||||
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
|
|
||||||
* __No Internet access:__ Internet is not used
|
|
||||||
* __Voice announcements__
|
|
||||||
* __No advertising__
|
|
||||||
* __Privacy:__
|
|
||||||
* _does not contain any in-app analytics_
|
|
||||||
* _does not use Google Play Services_
|
|
||||||
* _has no cloud integration_
|
|
||||||
|
|
||||||
__Only required permissions:__
|
|
||||||
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
|
||||||
|
|
||||||
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
|
|
||||||
|
|
||||||
_Please note:_
|
|
||||||
_OpenTracks_ does not contain a _map_ (as this would require internet access).
|
|
||||||
|
|
||||||
## Custom Dashboards (incl. Map)
|
|
||||||
As of v3.3.1 OpenTracks enables to use custom dashboards.
|
|
||||||
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
|
|
||||||
The developer documentation is in [README_API.md](README_API.md).
|
|
||||||
|
|
||||||
Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
|
|
||||||
However, this is rather slow and does not provide updates while recording.
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
<div>
|
<div>
|
||||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
|
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
|
||||||
@@ -96,12 +63,54 @@ However, this is rather slow and does not provide updates while recording.
|
|||||||
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
|
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* __Tracking:__ track your sport and outdoor activities
|
||||||
|
* __Voice announcements__
|
||||||
|
* __Photos and Markers:__ mark interesting locations while tracking
|
||||||
|
* __Export:__
|
||||||
|
* export tracks either as KMZ (incl. photos), KML, or GPX
|
||||||
|
* export automatically after each recording (e.g., to sync)
|
||||||
|
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||||
|
* __Altitude:__
|
||||||
|
* gain/loss via barometric sensor (if present)
|
||||||
|
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||||
|
* __Bluetooth LE sensors:__
|
||||||
|
* heart rate
|
||||||
|
* cycling: speed and distance
|
||||||
|
* cycling: cadence
|
||||||
|
* cycling: power meter
|
||||||
|
|
||||||
|
### Gadgetbridge integration
|
||||||
|
|
||||||
|
OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
|
||||||
|
* shows statistics via notification on smart watches (requires Gadgetbridge 0.56.1 or later), and
|
||||||
|
* Gadgetbridge's GPX exporter generates `opentracks:trackid` to avoid duplication (Gadgetbridge 0.53.0 or later).
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
* __No Internet access:__ Internet is not used
|
||||||
|
* __No advertising__
|
||||||
|
* __No in-app analytics__
|
||||||
|
* __No use of Google Play Services__
|
||||||
|
|
||||||
|
__Only required permission:__
|
||||||
|
* _ACCESS_FINE_LOCATION_: required to use the GPS.
|
||||||
|
|
||||||
|
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
|
||||||
|
|
||||||
|
## Custom Dashboards (incl. map)
|
||||||
|
As of v3.3.1, OpenTracks supports custom dashboards.
|
||||||
|
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
|
||||||
|
The developer documentation is in [README_API.md](README_API.md).
|
||||||
|
|
||||||
|
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
|
||||||
|
However, this is rather slow and does not provide updates while recording.
|
||||||
|
|
||||||
## Project history
|
## Project history
|
||||||
|
|
||||||
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
|
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
|
||||||
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
|
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
|
||||||
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
|
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
|
||||||
The [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
|
Then [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
|
||||||
Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
|
Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
|
||||||
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
|
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
|
||||||
|
|
||||||
@@ -114,7 +123,7 @@ Rework of _OpenTracks_ included:
|
|||||||
* removing calorie estimation and activity estimation,
|
* removing calorie estimation and activity estimation,
|
||||||
* removing support for ANT+ and Classic Bluetooth,
|
* removing support for ANT+ and Classic Bluetooth,
|
||||||
* adding support for Bluetooth LE heart rate sensors,
|
* adding support for Bluetooth LE heart rate sensors,
|
||||||
* removing Protobuf (store sensosensorr data in SQLite columns directly), and
|
* removing Protobuf (store sensor data in SQLite columns directly), and
|
||||||
* removing Android Service API for other apps.
|
* removing Android Service API for other apps.
|
||||||
|
|
||||||
Artwork, logos and user interface remained more or less unchanged.
|
Artwork, logos and user interface remained more or less unchanged.
|
||||||
|
|||||||
@@ -50,4 +50,5 @@ Also the distance is not computed.
|
|||||||
* QUARQ Red DZero Powermeter
|
* QUARQ Red DZero Powermeter
|
||||||
* Tacx Satori Smart
|
* Tacx Satori Smart
|
||||||
* Wahoo Kickr v4.0
|
* Wahoo Kickr v4.0
|
||||||
|
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.0'
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "de.dennisguse.opentracks"
|
applicationId "de.dennisguse.opentracks"
|
||||||
versionCode 4046
|
versionCode 4143
|
||||||
versionName "v3.18.0"
|
versionName "v3.18.3"
|
||||||
|
|
||||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
@@ -103,5 +103,5 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'org.mockito:mockito-android:3.9.0'
|
androidTestImplementation 'org.mockito:mockito-android:3.10.0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
v3.18.1: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Deleting tracks is handled by a service
|
||||||
|
- Uninstall deletes all data (i.e., hasFragileData=false)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Race condition: crash while recording and switching OpenTracks into background
|
||||||
|
- Search in track list was not working
|
||||||
|
- Cycling speed sensor: distance computation was wrong (only affected statistics)
|
||||||
|
|
||||||
|
Developer:
|
||||||
|
- ControllerFragment replaces TrackController
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
v3.18.2: OpenTracks
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Crash while moving from charts to another activity/app #787
|
||||||
|
|
||||||
|
Developer:
|
||||||
|
- TrackRecordingService pushes data to UI while recording
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
v3.18.3: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- KML parsing does not require order of when/gx:coords
|
||||||
|
- Track importer: reorders locations by timestamp
|
||||||
|
- Removed rendered launcher icons for Android < 8.0.0 (API 26)
|
||||||
|
(might look slightly different, but avoid problems for reproducible builds)
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Avoid crash in charts due to race condition
|
||||||
|
|
||||||
|
Developer:
|
||||||
|
- Split GPX and KML importer (removed common inheritance)
|
||||||
@@ -3,9 +3,9 @@ OpenTracks is a sport tracking application that completely respects your privacy
|
|||||||
Features:
|
Features:
|
||||||
* Sport tracking: track your sport and outdoor activities
|
* Sport tracking: track your sport and outdoor activities
|
||||||
* Photos and markers: mark interesting locations while tracking
|
* Photos and markers: mark interesting locations while tracking
|
||||||
* Supports Bluetooth LE sensors:
|
* Altitude gain and loss: via barometric sensor
|
||||||
* heart rate
|
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84
|
||||||
* cycling: speed and cadence
|
* Voice announcements
|
||||||
* cycling: power meter
|
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
|
||||||
* Export data: export tracks either as KML or GPX
|
* Export data: export tracks either as KMZ (incl. photos), KML, or GPX
|
||||||
* No Internet access: Internet is not used
|
* No Internet access: Internet is not used
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
|
||||||
|
import androidx.test.espresso.DataInteraction;
|
||||||
|
import androidx.test.espresso.UiController;
|
||||||
|
import androidx.test.espresso.ViewAction;
|
||||||
|
import androidx.test.espresso.ViewInteraction;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
import androidx.test.rule.ActivityTestRule;
|
||||||
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.TypeSafeMatcher;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onData;
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||||
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.anything;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Espresso test that checks the unit for activity category selected.
|
||||||
|
* Also it checks that unit is properly when user change from a pace activity to an speed one.
|
||||||
|
*/
|
||||||
|
@LargeTest
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class EspressoCategoryUnitTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ActivityTestRule<TrackListActivity> mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public GrantPermissionRule mGrantPermissionRule =
|
||||||
|
GrantPermissionRule.grant(
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changeBetweenCategoryUpdateProperlyUnitTest() {
|
||||||
|
// Start record.
|
||||||
|
ViewInteraction appCompatImageButton = onView(
|
||||||
|
allOf(withId(R.id.controller_record), withContentDescription("Record"),
|
||||||
|
childAtPosition(
|
||||||
|
allOf(withId(R.id.controller_container),
|
||||||
|
childAtPosition(
|
||||||
|
withId(R.id.controller_fragment),
|
||||||
|
0)),
|
||||||
|
0),
|
||||||
|
isDisplayed()));
|
||||||
|
appCompatImageButton.perform(click());
|
||||||
|
|
||||||
|
{
|
||||||
|
// Change to running activity (pace activity).
|
||||||
|
ViewInteraction appCompatImageView = onView(
|
||||||
|
allOf(withId(R.id.stats_activity_type_icon),
|
||||||
|
childAtPosition(
|
||||||
|
childAtPosition(
|
||||||
|
withClassName(is("android.widget.ScrollView")),
|
||||||
|
0),
|
||||||
|
7)));
|
||||||
|
appCompatImageView.perform(scrollTo(), click());
|
||||||
|
|
||||||
|
DataInteraction imageView = onData(anything())
|
||||||
|
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
|
||||||
|
childAtPosition(
|
||||||
|
withClassName(is("android.widget.LinearLayout")),
|
||||||
|
0)))
|
||||||
|
.atPosition(1);
|
||||||
|
imageView.perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pace (min/km) in all fields.
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
|
||||||
|
textView.perform(waitFor(2000));
|
||||||
|
textView.check(matches(withText("min/km")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
|
||||||
|
textView.check(matches(withText("min/km")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
|
||||||
|
textView.check(matches(withText("min/km")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
|
||||||
|
textView.check(matches(withText("min/km")));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Change to biking activity (speed activity).
|
||||||
|
ViewInteraction appCompatImageView = onView(
|
||||||
|
allOf(withId(R.id.stats_activity_type_icon),
|
||||||
|
childAtPosition(
|
||||||
|
childAtPosition(
|
||||||
|
withClassName(is("android.widget.ScrollView")),
|
||||||
|
0),
|
||||||
|
7)));
|
||||||
|
appCompatImageView.perform(scrollTo(), click());
|
||||||
|
|
||||||
|
DataInteraction imageView = onData(anything())
|
||||||
|
.inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view),
|
||||||
|
childAtPosition(
|
||||||
|
withClassName(is("android.widget.LinearLayout")),
|
||||||
|
0)))
|
||||||
|
.atPosition(9);
|
||||||
|
imageView.perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed (km/h) in all fields.
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit)));
|
||||||
|
textView.perform(waitFor(2000));
|
||||||
|
textView.check(matches(withText("km/h")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit)));
|
||||||
|
textView.check(matches(withText("km/h")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit)));
|
||||||
|
textView.check(matches(withText("km/h")));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit)));
|
||||||
|
textView.check(matches(withText("km/h")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Matcher<View> childAtPosition(
|
||||||
|
final Matcher<View> parentMatcher, final int position) {
|
||||||
|
|
||||||
|
return new TypeSafeMatcher<View>() {
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description) {
|
||||||
|
description.appendText("Child at position " + position + " in parent ");
|
||||||
|
parentMatcher.describeTo(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesSafely(View view) {
|
||||||
|
ViewParent parent = view.getParent();
|
||||||
|
return parent instanceof ViewGroup && parentMatcher.matches(parent)
|
||||||
|
&& view.equals(((ViewGroup) parent).getChildAt(position));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewAction waitFor(final long duration_ms) {
|
||||||
|
return new ViewAction() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Wait for milliseconds.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints() {
|
||||||
|
return isDisplayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, final View view) {
|
||||||
|
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.Instrumentation;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import androidx.test.espresso.Espresso;
|
||||||
|
import androidx.test.espresso.IdlingResource;
|
||||||
|
import androidx.test.espresso.UiController;
|
||||||
|
import androidx.test.espresso.ViewAction;
|
||||||
|
import androidx.test.espresso.ViewInteraction;
|
||||||
|
import androidx.test.espresso.action.MotionEvents;
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions;
|
||||||
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.hamcrest.Description;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.TypeSafeMatcher;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onData;
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.Espresso.pressBack;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
|
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||||
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.anything;
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class EspressoDeleteTrackTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
|
||||||
|
|
||||||
|
private MyIdlingResource idlingResource;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void registerIntentServiceIdlingResource() {
|
||||||
|
Instrumentation instrumentation
|
||||||
|
= InstrumentationRegistry.getInstrumentation();
|
||||||
|
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
|
||||||
|
Espresso.registerIdlingResources(idlingResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unregisterIntentServiceIdlingResource() {
|
||||||
|
Espresso.unregisterIdlingResources(idlingResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void espressoDeleteTrackTest() {
|
||||||
|
int countBefore;
|
||||||
|
|
||||||
|
{
|
||||||
|
// TrackListActivity: start recording
|
||||||
|
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||||
|
trackControllerRecordButton.perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// TrackRecordingActivity
|
||||||
|
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||||
|
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||||
|
|
||||||
|
// wait; stay recording
|
||||||
|
trackControllerRecordButton.perform(waitFor(5000));
|
||||||
|
|
||||||
|
// stop;
|
||||||
|
trackControllerStopButton.perform(veryLongTouch(1600));
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
pressBack();
|
||||||
|
|
||||||
|
// get number of items before deleting a track
|
||||||
|
countBefore = numberOfItemsListView();
|
||||||
|
|
||||||
|
// select track
|
||||||
|
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
|
||||||
|
|
||||||
|
// open menu and delete selected track
|
||||||
|
ViewInteraction overflowMenuButton = onView(
|
||||||
|
allOf(withContentDescription("More options"),
|
||||||
|
childAtPosition(
|
||||||
|
childAtPosition(
|
||||||
|
withId(R.id.toolbar),
|
||||||
|
2),
|
||||||
|
2),
|
||||||
|
isDisplayed()));
|
||||||
|
overflowMenuButton.perform(click());
|
||||||
|
|
||||||
|
ViewInteraction appCompatTextView = onView(
|
||||||
|
allOf(withId(R.id.title), withText("Delete"),
|
||||||
|
childAtPosition(
|
||||||
|
childAtPosition(
|
||||||
|
withId(R.id.content),
|
||||||
|
0),
|
||||||
|
0),
|
||||||
|
isDisplayed()));
|
||||||
|
appCompatTextView.perform(click());
|
||||||
|
|
||||||
|
ViewInteraction appCompatButton = onView(
|
||||||
|
allOf(withId(android.R.id.button1), withText("Yes"),
|
||||||
|
childAtPosition(
|
||||||
|
childAtPosition(
|
||||||
|
withId(R.id.buttonPanel),
|
||||||
|
0),
|
||||||
|
3)));
|
||||||
|
appCompatButton.perform(scrollTo(), click());
|
||||||
|
|
||||||
|
// check number of items after delete
|
||||||
|
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Matcher<View> childAtPosition(
|
||||||
|
final Matcher<View> parentMatcher, final int position) {
|
||||||
|
|
||||||
|
return new TypeSafeMatcher<View>() {
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description) {
|
||||||
|
description.appendText("Child at position " + position + " in parent ");
|
||||||
|
parentMatcher.describeTo(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesSafely(View view) {
|
||||||
|
ViewParent parent = view.getParent();
|
||||||
|
return parent instanceof ViewGroup && parentMatcher.matches(parent)
|
||||||
|
&& view.equals(((ViewGroup) parent).getChildAt(position));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Matcher<View> withListSize (final int size) {
|
||||||
|
return new TypeSafeMatcher<View> () {
|
||||||
|
@Override public boolean matchesSafely (final View view) {
|
||||||
|
return ((ListView) view).getCount () == size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void describeTo (final Description description) {
|
||||||
|
description.appendText ("ListView should have " + size + " items");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewAction waitFor(final long duration_ms) {
|
||||||
|
return new ViewAction() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Wait for milliseconds.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints() {
|
||||||
|
return isDisplayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, final View view) {
|
||||||
|
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||||
|
return new ViewAction() {
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Perform long touch.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Matcher<View> getConstraints() {
|
||||||
|
return isDisplayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void perform(UiController uiController, final View view) {
|
||||||
|
// Get view absolute position
|
||||||
|
int[] location = new int[2];
|
||||||
|
view.getLocationOnScreen(location);
|
||||||
|
|
||||||
|
// Offset coordinates by view position
|
||||||
|
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
|
||||||
|
|
||||||
|
// Send down event, pause, and send up
|
||||||
|
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
|
||||||
|
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||||
|
MotionEvents.sendUp(uiController, down, coordinates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int numberOfItemsListView() {
|
||||||
|
final int[] counts = new int[1];
|
||||||
|
onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher<View>() {
|
||||||
|
@Override
|
||||||
|
public boolean matchesSafely(View view) {
|
||||||
|
ListView listView = (ListView) view;
|
||||||
|
|
||||||
|
counts[0] = listView.getCount();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void describeTo(Description description) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return counts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MyIdlingResource implements IdlingResource {
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private ResourceCallback resourceCallback;
|
||||||
|
|
||||||
|
public MyIdlingResource(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return MyIdlingResource.class.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
|
||||||
|
this.resourceCallback = resourceCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIdleNow() {
|
||||||
|
boolean idle = !isDeleteServiceRunning();
|
||||||
|
if (idle && resourceCallback != null) {
|
||||||
|
resourceCallback.onTransitionToIdle();
|
||||||
|
}
|
||||||
|
return idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDeleteServiceRunning() {
|
||||||
|
ActivityManager manager =
|
||||||
|
(ActivityManager) context.getSystemService(
|
||||||
|
Context.ACTIVITY_SERVICE);
|
||||||
|
for (ActivityManager.RunningServiceInfo info :
|
||||||
|
manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||||
|
if (TrackDeleteService.class.getName().equals(
|
||||||
|
info.service.getClassName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,13 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onData;
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
import static androidx.test.espresso.action.ViewActions.click;
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static org.hamcrest.Matchers.anything;
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
@@ -39,13 +42,13 @@ public class EspressoUITest {
|
|||||||
public void record_pause_resume_stop() {
|
public void record_pause_resume_stop() {
|
||||||
{
|
{
|
||||||
// TrackListActivity: start recording
|
// TrackListActivity: start recording
|
||||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||||
trackControllerRecordButton.perform(click());
|
trackControllerRecordButton.perform(click());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// TrackRecordingActivity
|
// TrackRecordingActivity
|
||||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
|
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||||
|
|
||||||
// wait; stay recording
|
// wait; stay recording
|
||||||
trackControllerRecordButton.perform(waitFor(5000));
|
trackControllerRecordButton.perform(waitFor(5000));
|
||||||
@@ -72,13 +75,13 @@ public class EspressoUITest {
|
|||||||
public void record_move_through_tabs() {
|
public void record_move_through_tabs() {
|
||||||
{
|
{
|
||||||
// TrackListActivity: start recording
|
// TrackListActivity: start recording
|
||||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||||
trackControllerRecordButton.perform(click());
|
trackControllerRecordButton.perform(click());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
// TrackRecordingActivity
|
// TrackRecordingActivity
|
||||||
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
||||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
|
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||||
|
|
||||||
tabLayout.perform(selectTabAtIndex(1));
|
tabLayout.perform(selectTabAtIndex(1));
|
||||||
tabLayout.perform(waitFor(1000));
|
tabLayout.perform(waitFor(1000));
|
||||||
@@ -97,6 +100,13 @@ public class EspressoUITest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void selectAndDeleteTrack() {
|
||||||
|
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
|
||||||
|
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(2000));
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewAction veryLongTouch(final int duration_ms) {
|
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||||
return new ViewAction() {
|
return new ViewAction() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package de.dennisguse.opentracks.content.data;
|
package de.dennisguse.opentracks.content.data;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
@@ -25,4 +28,35 @@ public class TrackPointTest {
|
|||||||
|
|
||||||
assertFalse(tp.isRecent());
|
assertFalse(tp.isRecent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distanceToPrevious() {
|
||||||
|
Location l1 = new Location("test");
|
||||||
|
l1.setLatitude(0);
|
||||||
|
l1.setLongitude(0.0001);
|
||||||
|
TrackPoint tp1 = new TrackPoint(l1);
|
||||||
|
|
||||||
|
Location l2 = new Location("test");
|
||||||
|
l2.setLatitude(0);
|
||||||
|
l2.setLongitude(0.0002);
|
||||||
|
TrackPoint tp2 = new TrackPoint(l2);
|
||||||
|
|
||||||
|
// without sensor distance
|
||||||
|
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||||
|
|
||||||
|
// tp1 has sensor distance
|
||||||
|
tp1.setSensorDistance(Distance.of(5));
|
||||||
|
tp1.setSensorDistance(null);
|
||||||
|
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||||
|
|
||||||
|
// tp2 has sensor distance
|
||||||
|
tp1.setSensorDistance(null);
|
||||||
|
tp2.setSensorDistance(Distance.of(5));
|
||||||
|
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||||
|
|
||||||
|
// tp1 and tp2 have sensor distance
|
||||||
|
tp1.setSensorDistance(Distance.of(10));
|
||||||
|
tp2.setSensorDistance(Distance.of(5));
|
||||||
|
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
import de.dennisguse.opentracks.util.UintUtils;
|
import de.dennisguse.opentracks.util.UintUtils;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -100,7 +101,7 @@ public class SensorDataCyclingTest {
|
|||||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
current.compute(previous, 2150);
|
current.compute(previous, Distance.ofMM(2150));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
|
assertEquals(2.15, current.getValue().distance.toM(), 0.01);
|
||||||
@@ -114,7 +115,7 @@ public class SensorDataCyclingTest {
|
|||||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
current.compute(previous, 2000);
|
current.compute(previous, Distance.ofMM(2000));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(2, current.getValue().distance.toM(), 0.01);
|
assertEquals(2, current.getValue().distance.toM(), 0.01);
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks.fragments;
|
|
||||||
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.AfterClass;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.chart.ChartView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests {@link ChartFragment}.
|
|
||||||
*
|
|
||||||
* @author Youtao Liu
|
|
||||||
*/
|
|
||||||
//TODO Add tests that check ChartFragment
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ChartFragmentTest {
|
|
||||||
|
|
||||||
private static final double HOURS_PER_UNIT = 60.0;
|
|
||||||
|
|
||||||
private ChartFragment chartFragment;
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
public static void preSetUp() {
|
|
||||||
// Prepare looper for Android's message queue
|
|
||||||
if (Looper.myLooper() == null) Looper.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterClass
|
|
||||||
public static void finalTearDown() {
|
|
||||||
if (Looper.myLooper() != null) Looper.myLooper().quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
boolean chartByDistance = false;
|
|
||||||
chartFragment = ChartFragment.newInstance(chartByDistance);
|
|
||||||
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nothing() {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -90,10 +90,14 @@ public class ExportImportTest {
|
|||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
private Track.Id importTrackId;
|
private Track.Id importTrackId;
|
||||||
|
|
||||||
|
private TrackImporter trackImporter;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void fileSetup() throws IOException {
|
public void fileSetup() throws IOException {
|
||||||
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
|
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
|
||||||
tmpFileUri = Uri.fromFile(tmpFile);
|
tmpFileUri = Uri.fromFile(tmpFile);
|
||||||
|
|
||||||
|
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -169,8 +173,8 @@ public class ExportImportTest {
|
|||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -209,8 +213,8 @@ public class ExportImportTest {
|
|||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -248,8 +252,8 @@ public class ExportImportTest {
|
|||||||
contentProviderUtils.deleteTrack(context, trackId);
|
contentProviderUtils.deleteTrack(context, trackId);
|
||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
KmzTrackImporter trackImporter = new KmzTrackImporter();
|
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
|
||||||
importTrackId = trackImporter.importFile(context, tmpFileUri).get(0);
|
importTrackId = importer.importFile(tmpFileUri).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -288,8 +292,8 @@ public class ExportImportTest {
|
|||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||||
@@ -313,8 +317,8 @@ public class ExportImportTest {
|
|||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -362,8 +366,8 @@ public class ExportImportTest {
|
|||||||
|
|
||||||
// 2. import
|
// 2. import
|
||||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -406,18 +410,18 @@ public class ExportImportTest {
|
|||||||
// TODO Not exported for GPX/KML
|
// TODO Not exported for GPX/KML
|
||||||
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
||||||
|
|
||||||
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||||
if (trackPoint.hasLocation()) {
|
if (trackPoint.hasLocation()) {
|
||||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||||
}
|
}
|
||||||
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||||
if (trackPoint.hasSpeed()) {
|
if (trackPoint.hasSpeed()) {
|
||||||
assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
|
assertEquals("" + i, trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
|
||||||
}
|
}
|
||||||
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
assertEquals("" + i, trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
|
||||||
if (trackPoint.hasAltitude()) {
|
if (trackPoint.hasAltitude()) {
|
||||||
assertEquals(trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
|
assertEquals("" + i, trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
|
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
|
||||||
@@ -426,13 +430,13 @@ public class ExportImportTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (verifyHeartrate) {
|
if (verifyHeartrate) {
|
||||||
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||||
}
|
}
|
||||||
if (verifyCadence) {
|
if (verifyCadence) {
|
||||||
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||||
}
|
}
|
||||||
if (verifyPower) {
|
if (verifyPower) {
|
||||||
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||||
}
|
}
|
||||||
if (verifyAltitudeGain) {
|
if (verifyAltitudeGain) {
|
||||||
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
|
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.test.filters.LargeTest;
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.JUnit4;
|
import org.junit.runners.JUnit4;
|
||||||
@@ -15,6 +16,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
@@ -29,13 +31,20 @@ import static org.junit.Assert.assertNotNull;
|
|||||||
* Test that legacy KML/GPX formats can still be imported.
|
* Test that legacy KML/GPX formats can still be imported.
|
||||||
*/
|
*/
|
||||||
@RunWith(JUnit4.class)
|
@RunWith(JUnit4.class)
|
||||||
public class LegacyImportTest {
|
public class GPXImportTest {
|
||||||
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||||
|
|
||||||
|
private TrackImporter trackImporter;
|
||||||
|
|
||||||
private Track.Id importTrackId;
|
private Track.Id importTrackId;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
if (importTrackId != null) {
|
if (importTrackId != null) {
|
||||||
@@ -43,63 +52,6 @@ public class LegacyImportTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that data with statistics markers is imported and those ignored.
|
|
||||||
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
|
||||||
*/
|
|
||||||
@LargeTest
|
|
||||||
@Test
|
|
||||||
public void kml_with_statistics_marker() throws IOException {
|
|
||||||
// given
|
|
||||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
|
||||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
|
|
||||||
|
|
||||||
// when
|
|
||||||
// 1. import
|
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
|
||||||
|
|
||||||
// then
|
|
||||||
// 1. track
|
|
||||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
|
||||||
assertNotNull(importedTrack);
|
|
||||||
assertEquals("unknown", importedTrack.getCategory());
|
|
||||||
assertEquals("Test Track", importedTrack.getDescription());
|
|
||||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
|
||||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
|
||||||
|
|
||||||
// 2. markers
|
|
||||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
|
||||||
|
|
||||||
// 3. trackpoints
|
|
||||||
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
|
||||||
assertEquals(6, importedTrackPoints.size());
|
|
||||||
|
|
||||||
// first 3 trackpoints
|
|
||||||
assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
|
||||||
assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
|
||||||
assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
|
||||||
|
|
||||||
// created resume trackpoint with time of next valid trackpoint
|
|
||||||
assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
|
||||||
assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
|
||||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* At least one valid location is required.
|
|
||||||
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
|
||||||
*/
|
|
||||||
@LargeTest
|
|
||||||
@Test(expected = ImportParserException.class)
|
|
||||||
public void kml_without_locations() throws IOException {
|
|
||||||
// given
|
|
||||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
|
||||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
|
|
||||||
|
|
||||||
// when
|
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
|
* Check that data that contains pause (lat=100, lng=0) and resume (lat=200, lng=0) locations are restored to a segment break.
|
||||||
*/
|
*/
|
||||||
@@ -107,12 +59,12 @@ public class LegacyImportTest {
|
|||||||
@Test
|
@Test
|
||||||
public void gpx_with_pause_resume() throws IOException {
|
public void gpx_with_pause_resume() throws IOException {
|
||||||
// given
|
// given
|
||||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context));
|
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
// 1. import
|
// 1. import
|
||||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
// 1. track
|
// 1. track
|
||||||
@@ -138,7 +90,7 @@ public class LegacyImportTest {
|
|||||||
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
|
assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2021-01-07T21:52:04.103Z", 14.003, 3.0, 10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
static void assertTrackpoint(final TrackPoint trackPoint, final TrackPoint.Type type, final String when, final Double longitude, final Double latitude, final Double altitude) {
|
||||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||||
assertEquals(type, trackPoint.getType());
|
assertEquals(type, trackPoint.getType());
|
||||||
|
|
||||||
@@ -154,6 +106,5 @@ public class LegacyImportTest {
|
|||||||
} else {
|
} else {
|
||||||
assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
|
assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that legacy KML/GPX formats can still be imported.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class KMLImportTest {
|
||||||
|
|
||||||
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||||
|
|
||||||
|
private TrackImporter trackImporter;
|
||||||
|
|
||||||
|
private Track.Id importTrackId;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
if (importTrackId != null) {
|
||||||
|
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KML does not specify the order of gx:coords and when; only count has to be the same.
|
||||||
|
*/
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void kml22_order_location_and_when() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
|
||||||
|
|
||||||
|
// when
|
||||||
|
// 1. import
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
|
// then
|
||||||
|
// 1. track
|
||||||
|
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||||
|
assertNotNull(importedTrack);
|
||||||
|
assertEquals("", importedTrack.getCategory());
|
||||||
|
assertEquals("", importedTrack.getDescription());
|
||||||
|
assertEquals("", importedTrack.getName());
|
||||||
|
assertEquals("", importedTrack.getIcon());
|
||||||
|
|
||||||
|
// 2. markers
|
||||||
|
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||||
|
|
||||||
|
// 3. trackpoints
|
||||||
|
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||||
|
assertEquals(5, importedTrackPoints.size());
|
||||||
|
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:21.767Z", null, null, null);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.042Z", 14.0, 3.0, 10.0);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.192Z", 14.001, 3.0, 10.0);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:22.318Z", 14.002, 3.0, 10.0);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.SEGMENT_END_MANUAL, "2021-05-29T18:06:22.512Z", null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates / when might not be ordered by increasing time.
|
||||||
|
*/
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void kml22_time_decreases() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
|
||||||
|
|
||||||
|
// when
|
||||||
|
// 1. import
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
|
// then
|
||||||
|
// 1. track
|
||||||
|
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||||
|
assertNotNull(importedTrack);
|
||||||
|
assertEquals("", importedTrack.getCategory());
|
||||||
|
assertEquals("", importedTrack.getDescription());
|
||||||
|
assertEquals("", importedTrack.getName());
|
||||||
|
assertEquals("", importedTrack.getIcon());
|
||||||
|
|
||||||
|
// 2. markers
|
||||||
|
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||||
|
|
||||||
|
// 3. trackpoints
|
||||||
|
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||||
|
assertEquals(2, importedTrackPoints.size());
|
||||||
|
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.TRACKPOINT, "2021-05-29T18:06:21.767Z", 14.0, 3.0, 10.0);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.SEGMENT_START_MANUAL, "2021-05-29T18:06:22.042Z", null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that data with statistics markers is imported and those ignored.
|
||||||
|
* Statistics marker were created to avoid recomputing the track statistics (e.g., distance until a certain time).
|
||||||
|
*/
|
||||||
|
@LargeTest
|
||||||
|
@Test
|
||||||
|
public void kml22_with_statistics_marker() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
|
||||||
|
|
||||||
|
// when
|
||||||
|
// 1. import
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
|
||||||
|
// then
|
||||||
|
// 1. track
|
||||||
|
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||||
|
assertNotNull(importedTrack);
|
||||||
|
assertEquals("unknown", importedTrack.getCategory());
|
||||||
|
assertEquals("Test Track", importedTrack.getDescription());
|
||||||
|
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||||
|
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||||
|
|
||||||
|
// 2. markers
|
||||||
|
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||||
|
|
||||||
|
// 3. trackpoints
|
||||||
|
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||||
|
assertEquals(6, importedTrackPoints.size());
|
||||||
|
|
||||||
|
// first 3 trackpoints
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(0), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(1), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(2), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||||
|
|
||||||
|
// created resume trackpoint with time of next valid trackpoint
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(3), TrackPoint.Type.SEGMENT_START_AUTOMATIC, "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(4), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||||
|
GPXImportTest.assertTrackpoint(importedTrackPoints.get(5), TrackPoint.Type.TRACKPOINT, "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* At least one valid location is required.
|
||||||
|
* Before v3.15.0, Tracks without TrackPoints could be created; such tracks cannot be imported as we cannot restore the TrackStatistics (especially startTime and stopTime).
|
||||||
|
*/
|
||||||
|
@LargeTest
|
||||||
|
@Test(expected = ImportParserException.class)
|
||||||
|
public void kml_without_locations() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
|
||||||
|
|
||||||
|
// when
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@LargeTest
|
||||||
|
@Test(expected = ImportParserException.class)
|
||||||
|
public void kml_when_locations_different() throws IOException {
|
||||||
|
// given
|
||||||
|
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
|
||||||
|
|
||||||
|
// when
|
||||||
|
importTrackId = importer.importFile(inputStream).get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,6 +146,34 @@ public class TrackStatisticsUpdaterTest {
|
|||||||
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
|
||||||
|
// given
|
||||||
|
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
|
||||||
|
|
||||||
|
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
|
||||||
|
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
|
||||||
|
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
|
||||||
|
tp3.setSensorDistance(Distance.of(5f));
|
||||||
|
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
|
||||||
|
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
|
||||||
|
|
||||||
|
// when
|
||||||
|
subject.addTrackPoint(tp1, GPS_DISTANCE);
|
||||||
|
subject.addTrackPoint(tp2, GPS_DISTANCE);
|
||||||
|
subject.addTrackPoint(tp3, GPS_DISTANCE);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||||
|
|
||||||
|
// when
|
||||||
|
subject.addTrackPoint(tp4, GPS_DISTANCE);
|
||||||
|
subject.addTrackPoint(tp5, GPS_DISTANCE);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
|
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
|
||||||
@Test
|
@Test
|
||||||
public void addTrackPoint_speed_from_GPS_not_moving() {
|
public void addTrackPoint_speed_from_GPS_not_moving() {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||||
|
xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document>
|
||||||
|
<Placemark>
|
||||||
|
<gx:MultiTrack>
|
||||||
|
<altitudeMode>absolute</altitudeMode>
|
||||||
|
<gx:interpolate>1</gx:interpolate>
|
||||||
|
<gx:Track>
|
||||||
|
<when>2021-05-29T18:06:21.767Z</when>
|
||||||
|
<gx:coord />
|
||||||
|
|
||||||
|
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||||
|
<when>2021-05-29T18:06:22.042Z</when>
|
||||||
|
|
||||||
|
<when>2021-05-29T18:06:22.192Z</when>
|
||||||
|
<when>2021-05-29T18:06:22.318Z</when>
|
||||||
|
<when>2021-05-29T18:06:22.512Z</when>
|
||||||
|
|
||||||
|
<gx:coord>14.001 3.0 10.0</gx:coord>
|
||||||
|
<gx:coord>14.002 3.0 10.0</gx:coord>
|
||||||
|
<gx:coord />
|
||||||
|
</gx:Track>
|
||||||
|
</gx:MultiTrack>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||||
|
xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document>
|
||||||
|
<Placemark>
|
||||||
|
<gx:MultiTrack>
|
||||||
|
<altitudeMode>absolute</altitudeMode>
|
||||||
|
<gx:interpolate>1</gx:interpolate>
|
||||||
|
<gx:Track>
|
||||||
|
<when>2021-05-29T18:06:22.042Z</when>
|
||||||
|
<gx:coord />
|
||||||
|
|
||||||
|
<when>2021-05-29T18:06:21.767Z</when>
|
||||||
|
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||||
|
</gx:Track>
|
||||||
|
</gx:MultiTrack>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||||
|
xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document>
|
||||||
|
<Placemark>
|
||||||
|
<gx:MultiTrack>
|
||||||
|
<altitudeMode>absolute</altitudeMode>
|
||||||
|
<gx:interpolate>1</gx:interpolate>
|
||||||
|
<gx:Track>
|
||||||
|
<when>2021-05-29T18:06:21.767Z</when>
|
||||||
|
<gx:coord />
|
||||||
|
|
||||||
|
<gx:coord>14.0 3.0 10.0</gx:coord>
|
||||||
|
</gx:Track>
|
||||||
|
</gx:MultiTrack>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kml xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||||
|
xmlns="http://www.opengis.net/kml/2.2">
|
||||||
|
<Document>
|
||||||
|
<Placemark>
|
||||||
|
<gx:MultiTrack>
|
||||||
|
<altitudeMode>absolute</altitudeMode>
|
||||||
|
<gx:interpolate>1</gx:interpolate>
|
||||||
|
<gx:Track />
|
||||||
|
</gx:MultiTrack>
|
||||||
|
</Placemark>
|
||||||
|
</Document>
|
||||||
|
</kml>
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
|
||||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
||||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
|
||||||
<Document>
|
|
||||||
<open>1</open>
|
|
||||||
<visibility>1</visibility>
|
|
||||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
|
||||||
<atom:generator><![CDATA[OpenTracks]]></atom:generator>
|
|
||||||
<Style id="track">
|
|
||||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
|
||||||
<IconStyle>
|
|
||||||
<scale>1.3</scale>
|
|
||||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
|
||||||
</IconStyle>
|
|
||||||
</Style>
|
|
||||||
<Style id="start"><IconStyle>
|
|
||||||
<scale>1.3</scale>
|
|
||||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
|
||||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
|
||||||
</IconStyle></Style>
|
|
||||||
<Style id="end"><IconStyle>
|
|
||||||
<scale>1.3</scale>
|
|
||||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
|
||||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
|
||||||
</IconStyle></Style>
|
|
||||||
<Style id="waypoint"><IconStyle>
|
|
||||||
<scale>1.3</scale>
|
|
||||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
|
||||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
|
||||||
</IconStyle></Style>
|
|
||||||
<Schema id="schema">
|
|
||||||
<gx:SimpleArrayField name="speed" type="float">
|
|
||||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
|
||||||
</gx:SimpleArrayField>
|
|
||||||
<gx:SimpleArrayField name="power" type="float">
|
|
||||||
<displayName><![CDATA[Power (W)]]></displayName>
|
|
||||||
</gx:SimpleArrayField>
|
|
||||||
<gx:SimpleArrayField name="cadence" type="float">
|
|
||||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
|
||||||
</gx:SimpleArrayField>
|
|
||||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
|
||||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
|
||||||
</gx:SimpleArrayField>
|
|
||||||
</Schema>
|
|
||||||
<Placemark>
|
|
||||||
<name><![CDATA[2020-08-24 20:57]]></name>
|
|
||||||
<description><![CDATA[]]></description>
|
|
||||||
<icon><![CDATA[UNKNOWN]]></icon>
|
|
||||||
<opentracks:trackid>9316e527-a5b8-47c5-bdbd-954b4be7d80e</opentracks:trackid>
|
|
||||||
<styleUrl>#track</styleUrl>
|
|
||||||
<ExtendedData>
|
|
||||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
|
||||||
</ExtendedData>
|
|
||||||
<gx:MultiTrack>
|
|
||||||
<altitudeMode>absolute</altitudeMode>
|
|
||||||
<gx:interpolate>1</gx:interpolate>
|
|
||||||
</gx:MultiTrack>
|
|
||||||
</Placemark>
|
|
||||||
</Document>
|
|
||||||
</kml>
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:hasFragileUserData="true"
|
||||||
|
tools:replace="android:hasFragileUserData" />
|
||||||
|
</manifest>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -51,18 +51,14 @@ limitations under the License.
|
|||||||
android:name=".Startup"
|
android:name=".Startup"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/ThemeCustom">
|
android:theme="@style/ThemeCustom">
|
||||||
|
|
||||||
<activity android:name=".AboutActivity" />
|
<activity android:name=".AboutActivity" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".TrackDeleteActivity"
|
|
||||||
android:theme="@style/TrackDeleteActivity" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HelpActivity"
|
android:name=".HelpActivity"
|
||||||
android:label="@string/menu_help" />
|
android:label="@string/menu_help" />
|
||||||
@@ -350,6 +346,13 @@ limitations under the License.
|
|||||||
android:name=".io.file.importer.ImportService"
|
android:name=".io.file.importer.ImportService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.TrackDeleteService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:icon="@drawable/ic_logo_color_24dp"
|
||||||
|
android:label="@string/delete_service" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,33 +24,38 @@ import java.util.Arrays;
|
|||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||||
|
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||||
|
import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class for the following common tasks across
|
* An abstract class for the following common tasks across
|
||||||
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
||||||
* <p>
|
* <p>
|
||||||
* - share track <br>
|
|
||||||
* - delete tracks <br>
|
* - delete tracks <br>
|
||||||
*
|
*
|
||||||
* @author Jimmy Shih
|
* @author Jimmy Shih
|
||||||
*/
|
*/
|
||||||
//TODO It is actually not a List; we need a better name for this class
|
|
||||||
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
|
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
|
||||||
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
|
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
|
||||||
|
|
||||||
private static final String TAG = AbstractListActivity.class.getSimpleName();
|
private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
|
||||||
|
|
||||||
protected static final int GPS_REQUEST_CODE = 6;
|
private TrackDeleteServiceConnection trackDeleteServiceConnection;
|
||||||
private static final int DELETE_REQUEST_CODE = 3;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onStart() {
|
||||||
if (requestCode == DELETE_REQUEST_CODE) {
|
super.onStart();
|
||||||
onTrackDeleted();
|
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||||
} else {
|
trackDeleteServiceConnection.bind(this);
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
if (trackDeleteServiceConnection != null) {
|
||||||
|
trackDeleteServiceConnection.unbind(this);
|
||||||
|
trackDeleteServiceConnection = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +72,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
|||||||
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
||||||
boolean stopRecording = false;
|
boolean stopRecording = false;
|
||||||
|
|
||||||
|
onDeleteConfirmed();
|
||||||
|
|
||||||
for (Track.Id trackId : trackIds) {
|
for (Track.Id trackId : trackIds) {
|
||||||
if (trackId.equals(getRecordingTrackId())) {
|
if (trackId.equals(getRecordingTrackId())) {
|
||||||
stopRecording = true;
|
stopRecording = true;
|
||||||
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
|||||||
if (stopRecording) {
|
if (stopRecording) {
|
||||||
getTrackRecordingServiceConnection().stopRecording(this);
|
getTrackRecordingServiceConnection().stopRecording(this);
|
||||||
}
|
}
|
||||||
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
|
|
||||||
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
|
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||||
startActivityForResult(intent, DELETE_REQUEST_CODE);
|
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,12 +98,28 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after {@link TrackDeleteActivity} returns its result.
|
* Called every time a track is deleted.
|
||||||
*/
|
*/
|
||||||
abstract protected void onTrackDeleted();
|
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||||
|
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
||||||
|
trackDeleteServiceConnection.unbind(this);
|
||||||
|
trackDeleteServiceConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onDeleteConfirmed();
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected Track.Id getRecordingTrackId() {
|
protected Track.Id getRecordingTrackId() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connected() {
|
||||||
|
TrackDeleteService service = trackDeleteServiceConnection.getServiceIfBound();
|
||||||
|
if (service == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, status -> onTrackDeleteStatus(status));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.drawable.TransitionDrawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
|
||||||
|
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||||
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
|
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||||
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track controller for record, pause, resume, and stop.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
//TODO Move buttons into separate components
|
||||||
|
public class ControllerFragment extends Fragment implements View.OnTouchListener {
|
||||||
|
|
||||||
|
private static final String TAG = ControllerFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private Handler handlerUpdateTotalTime;
|
||||||
|
|
||||||
|
private Runnable buttonDelay;
|
||||||
|
private int buttonDelayDuration;
|
||||||
|
|
||||||
|
private ControllerFragmentBinding viewBinding;
|
||||||
|
private TransitionDrawable transition;
|
||||||
|
|
||||||
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
|
private TrackRecordingService.RecordingStatus recordingStatus;
|
||||||
|
private TrackRecordingService.RecordingData recordingData;
|
||||||
|
|
||||||
|
private final Runnable bindChangedCallback = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||||
|
if (service == null) {
|
||||||
|
Log.w(TAG, "could not get TrackRecordingService");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.getRecordingStatusObservable()
|
||||||
|
.observe(ControllerFragment.this, status -> onRecordingStatusChanged(status));
|
||||||
|
|
||||||
|
service.getRecordingDataObservable()
|
||||||
|
.observe(ControllerFragment.this, recordingData -> onTotalTimeChanged(recordingData));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
|
||||||
|
|
||||||
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||||
|
handlerUpdateTotalTime = new Handler();
|
||||||
|
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
|
viewBinding.controllerRecord.setOnTouchListener(this);
|
||||||
|
viewBinding.controllerRecord.setOnClickListener((view) -> {
|
||||||
|
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCallback().recordStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
viewBinding.controllerStop.setOnTouchListener(this);
|
||||||
|
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
|
||||||
|
|
||||||
|
onRecordingStatusChanged(recordingStatus);
|
||||||
|
|
||||||
|
return viewBinding.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
trackRecordingServiceConnection.startConnection(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
trackRecordingServiceConnection.unbind(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
|
transition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
trackRecordingServiceConnection = null;
|
||||||
|
handlerUpdateTotalTime = null;
|
||||||
|
recordingStatus = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO Move into a separate object
|
||||||
|
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(final View view, MotionEvent event) {
|
||||||
|
if (!isResumed()) {
|
||||||
|
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
||||||
|
buttonDelay = null;
|
||||||
|
view.setPressed(false);
|
||||||
|
|
||||||
|
transition.resetTransition();
|
||||||
|
|
||||||
|
showStatusSetDefaultText();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Trigger button pressed animation
|
||||||
|
view.setPressed(true);
|
||||||
|
|
||||||
|
//To pause a recording
|
||||||
|
if (viewBinding.controllerRecord.equals(view)
|
||||||
|
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||||
|
&& recordingStatus.isRecordingAndNotPaused()) {
|
||||||
|
|
||||||
|
transition.startTransition(buttonDelayDuration);
|
||||||
|
|
||||||
|
ActivityUtils.vibrate(getContext(), 150);
|
||||||
|
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
|
||||||
|
|
||||||
|
buttonDelay = () -> {
|
||||||
|
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||||
|
view.performClick();
|
||||||
|
getCallback().recordPause();
|
||||||
|
|
||||||
|
transition.resetTransition();
|
||||||
|
|
||||||
|
ActivityUtils.vibrate(getContext(), 1000);
|
||||||
|
};
|
||||||
|
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//To stop a recording
|
||||||
|
if (viewBinding.controllerStop.equals(view)
|
||||||
|
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||||
|
&& recordingStatus.isRecording()) {
|
||||||
|
|
||||||
|
|
||||||
|
transition.startTransition(buttonDelayDuration);
|
||||||
|
|
||||||
|
ActivityUtils.vibrate(getContext(), 150);
|
||||||
|
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
|
||||||
|
|
||||||
|
buttonDelay = () -> {
|
||||||
|
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||||
|
getCallback().recordStop();
|
||||||
|
ActivityUtils.vibrate(getContext(), 1000);
|
||||||
|
|
||||||
|
transition.resetTransition();
|
||||||
|
};
|
||||||
|
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
viewBinding.controllerContainer.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showStatusSetDefaultText() {
|
||||||
|
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Callback getCallback() {
|
||||||
|
return (Callback) getActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void recordStart();
|
||||||
|
|
||||||
|
void recordPause();
|
||||||
|
|
||||||
|
void recordStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||||
|
recordingStatus = status;
|
||||||
|
|
||||||
|
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||||
|
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
|
||||||
|
|
||||||
|
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
|
||||||
|
|
||||||
|
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||||
|
if (recordingStatus.isRecording()) {
|
||||||
|
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
|
||||||
|
showStatusSetDefaultText();
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onTotalTimeChanged(TrackRecordingService.RecordingData recordingData) {
|
||||||
|
this.recordingData = recordingData;
|
||||||
|
|
||||||
|
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(this.recordingData.getTrackStatistics().getTotalTime()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,7 +175,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||||
|
|
||||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
|
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
@@ -261,15 +261,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
|
if (!searchView.isIconified()) {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (loaderCallbacks.getSearchQuery() != null) {
|
if (loaderCallbacks.getSearchQuery() != null) {
|
||||||
loaderCallbacks.setSearch(null);
|
loaderCallbacks.setSearch(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
|
||||||
if (!searchView.isIconified()) {
|
|
||||||
searchView.setIconified(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.drawable.TransitionDrawable;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
|
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
|
||||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track controller for record, pause, resume, and stop.
|
|
||||||
*
|
|
||||||
* @author Jimmy Shih
|
|
||||||
*/
|
|
||||||
//TODO Subscribe to TrackRecordingService.recordingStatus
|
|
||||||
//TODO Could be a fragment
|
|
||||||
public class TrackController implements View.OnTouchListener {
|
|
||||||
|
|
||||||
private static final String TAG = TrackController.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
|
||||||
|
|
||||||
private final Activity activity;
|
|
||||||
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
|
|
||||||
private final Handler handlerUpdateTotalTime = new Handler();
|
|
||||||
private final boolean alwaysShow;
|
|
||||||
|
|
||||||
private Runnable buttonDelay;
|
|
||||||
private final int buttonDelayDuration;
|
|
||||||
|
|
||||||
private final TrackControllerBinding viewBinding;
|
|
||||||
private final TransitionDrawable transition;
|
|
||||||
|
|
||||||
|
|
||||||
private TrackRecordingService.RecordingStatus recordingStatus;
|
|
||||||
private Duration totalTime;
|
|
||||||
|
|
||||||
private final Callback callback;
|
|
||||||
|
|
||||||
// A runnable to update the total time.
|
|
||||||
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
|
|
||||||
updateTotalTime();
|
|
||||||
setTotalTime();
|
|
||||||
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.viewBinding = viewBinding;
|
|
||||||
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
|
|
||||||
this.alwaysShow = alwaysShow;
|
|
||||||
this.callback = callback;
|
|
||||||
|
|
||||||
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
|
|
||||||
|
|
||||||
viewBinding.trackControllerRecord.setOnTouchListener(this);
|
|
||||||
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
|
|
||||||
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.recordStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
viewBinding.trackControllerStop.setOnTouchListener(this);
|
|
||||||
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(final View view, MotionEvent event) {
|
|
||||||
if (!isResumed()) {
|
|
||||||
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
|
||||||
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
|
||||||
buttonDelay = null;
|
|
||||||
view.setPressed(false);
|
|
||||||
|
|
||||||
transition.resetTransition();
|
|
||||||
|
|
||||||
showStatusSetDefaultText();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Trigger button pressed animation
|
|
||||||
view.setPressed(true);
|
|
||||||
|
|
||||||
//To pause a recording
|
|
||||||
if (viewBinding.trackControllerRecord.equals(view)
|
|
||||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
|
||||||
&& recordingStatus.isRecordingAndNotPaused()) {
|
|
||||||
|
|
||||||
transition.startTransition(buttonDelayDuration);
|
|
||||||
|
|
||||||
ActivityUtils.vibrate(activity, 150);
|
|
||||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
|
|
||||||
|
|
||||||
buttonDelay = () -> {
|
|
||||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
|
||||||
view.performClick();
|
|
||||||
callback.recordPause();
|
|
||||||
|
|
||||||
transition.resetTransition();
|
|
||||||
|
|
||||||
ActivityUtils.vibrate(activity, 1000);
|
|
||||||
};
|
|
||||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//To stop a recording
|
|
||||||
if (viewBinding.trackControllerStop.equals(view)
|
|
||||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
|
||||||
&& recordingStatus.isRecording()) {
|
|
||||||
|
|
||||||
|
|
||||||
transition.startTransition(buttonDelayDuration);
|
|
||||||
|
|
||||||
ActivityUtils.vibrate(activity, 150);
|
|
||||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
|
|
||||||
|
|
||||||
buttonDelay = () -> {
|
|
||||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
|
||||||
callback.recordStop();
|
|
||||||
ActivityUtils.vibrate(activity, 1000);
|
|
||||||
|
|
||||||
transition.resetTransition();
|
|
||||||
};
|
|
||||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void update(TrackRecordingService.RecordingStatus recordingStatus) {
|
|
||||||
if (!isResumed()) {
|
|
||||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.recordingStatus = recordingStatus;
|
|
||||||
boolean isRecording = recordingStatus.isRecording();
|
|
||||||
boolean isRecordingPaused = recordingStatus.isPaused();
|
|
||||||
boolean visible = alwaysShow || isRecording;
|
|
||||||
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
if (!visible) {
|
|
||||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRecording && !isRecordingPaused) {
|
|
||||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UI_UPDATE_INTERVAL.toMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
|
||||||
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
|
|
||||||
|
|
||||||
viewBinding.trackControllerStop.setEnabled(isRecording);
|
|
||||||
|
|
||||||
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
|
||||||
if (isRecording) {
|
|
||||||
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
|
|
||||||
showStatusSetDefaultText();
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
|
||||||
|
|
||||||
if (recordingStatus.isRecording()) {
|
|
||||||
updateTotalTime();
|
|
||||||
setTotalTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onResume(TrackRecordingService.RecordingStatus recordingStatus) {
|
|
||||||
this.recordingStatus = recordingStatus;
|
|
||||||
update(recordingStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPause() {
|
|
||||||
recordingStatus = null;
|
|
||||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hide() {
|
|
||||||
viewBinding.trackControllerContainer.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTotalTime() {
|
|
||||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
|
||||||
if (trackRecordingService != null) {
|
|
||||||
totalTime = trackRecordingService.getTotalTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTotalTime() {
|
|
||||||
if (totalTime != null) {
|
|
||||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showStatusSetDefaultText() {
|
|
||||||
viewBinding.trackControllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isResumed() {
|
|
||||||
return recordingStatus != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Callback {
|
|
||||||
void recordStart();
|
|
||||||
|
|
||||||
void recordPause();
|
|
||||||
|
|
||||||
void recordStop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2013 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|
||||||
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
|
|
||||||
import de.dennisguse.opentracks.util.SystemUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An activity for deleting tracks.
|
|
||||||
*
|
|
||||||
* @author Jimmy Shih
|
|
||||||
*/
|
|
||||||
public class TrackDeleteActivity extends AbstractActivity {
|
|
||||||
|
|
||||||
public static final String EXTRA_TRACK_IDS = "track_ids";
|
|
||||||
|
|
||||||
private List<Track.Id> trackIds;
|
|
||||||
|
|
||||||
private Thread deleteThread;
|
|
||||||
|
|
||||||
private TrackDeleteBinding viewBinding;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
|
||||||
deleteThread = new Thread(() -> {
|
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
|
|
||||||
|
|
||||||
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
|
|
||||||
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
|
|
||||||
|
|
||||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
|
||||||
if (Thread.interrupted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
|
|
||||||
});
|
|
||||||
deleteThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
viewBinding = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
if (deleteThread.getState() == Thread.State.TERMINATED) {
|
|
||||||
onAsyncTaskCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
deleteThread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected View getRootView() {
|
|
||||||
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
|
|
||||||
return viewBinding.getRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onAsyncTaskCompleted() {
|
|
||||||
viewBinding.progressbar.setVisibility(View.INVISIBLE);
|
|
||||||
setResult(RESULT_OK);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -71,14 +71,15 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
|||||||
*
|
*
|
||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
*/
|
*/
|
||||||
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
|
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
|
||||||
|
|
||||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
protected static final int GPS_REQUEST_CODE = 6;
|
||||||
|
|
||||||
// The following are set in onCreate
|
// The following are set in onCreate
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
private TrackController trackController;
|
|
||||||
private ResourceCursorAdapter resourceCursorAdapter;
|
private ResourceCursorAdapter resourceCursorAdapter;
|
||||||
|
|
||||||
private TrackListBinding viewBinding;
|
private TrackListBinding viewBinding;
|
||||||
@@ -118,7 +119,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
TrackListActivity.this.invalidateOptionsMenu();
|
TrackListActivity.this.invalidateOptionsMenu();
|
||||||
loaderCallbacks.restart();
|
loaderCallbacks.restart();
|
||||||
trackController.onResume(recordingStatus);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,9 +132,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
private final Runnable bindChangedCallback = new Runnable() {
|
private final Runnable bindChangedCallback = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// After binding changes (e.g., becomes available), update the total time in trackController.
|
|
||||||
runOnUiThread(() -> trackController.update(recordingStatus));
|
|
||||||
|
|
||||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||||
if (service == null) {
|
if (service == null) {
|
||||||
Log.e(TAG, "service not available to start gps or a new recording");
|
Log.e(TAG, "service not available to start gps or a new recording");
|
||||||
@@ -170,14 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||||
|
|
||||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
|
|
||||||
|
|
||||||
|
|
||||||
// Show trackController when search dialog is dismissed
|
|
||||||
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
|
|
||||||
if (searchManager != null) {
|
|
||||||
searchManager.setOnDismissListener(() -> trackController.show());
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||||
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||||
@@ -250,14 +239,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
trackRecordingServiceConnection.startConnection(this);
|
trackRecordingServiceConnection.startConnection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
|
|
||||||
// Update UI
|
|
||||||
trackController.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -265,7 +246,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
// Update UI
|
// Update UI
|
||||||
this.invalidateOptionsMenu();
|
this.invalidateOptionsMenu();
|
||||||
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
||||||
trackController.onResume(recordingStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -298,7 +278,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||||
|
|
||||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
|
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||||
|
searchView.setOnCloseListener(() -> {
|
||||||
|
searchView.clearFocus();
|
||||||
|
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
@@ -306,7 +292,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
updateMenuItems(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
|
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
|
||||||
|
|
||||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
searchView.setQuery("", false);
|
searchView.setQuery("", false);
|
||||||
@@ -355,6 +341,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.getItemId() == R.id.track_list_search) {
|
||||||
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
|
searchView.setIconified(false);
|
||||||
|
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,17 +358,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void overridePendingTransition(int enterAnim, int exitAnim) {
|
||||||
|
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
|
if (!searchView.isIconified()) {
|
||||||
|
searchView.setIconified(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (loaderCallbacks.getSearchQuery() != null) {
|
if (loaderCallbacks.getSearchQuery() != null) {
|
||||||
loaderCallbacks.setSearch(null);
|
loaderCallbacks.setSearch(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
|
||||||
if (!searchView.isIconified()) {
|
|
||||||
searchView.setIconified(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,20 +394,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
loaderCallbacks.setSearch(searchQuery);
|
loaderCallbacks.setSearch(searchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSearchRequested() {
|
|
||||||
// Hide trackController when search dialog is shown
|
|
||||||
trackController.hide();
|
|
||||||
return super.onSearchRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||||
return trackRecordingServiceConnection;
|
return trackRecordingServiceConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTrackDeleted() {
|
protected void onDeleteConfirmed() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,22 +416,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the menu items with not fixed icon for gps option.
|
|
||||||
*
|
|
||||||
* @param isGpsStarted true if gps is started
|
|
||||||
* @param isRecording true if recording
|
|
||||||
*/
|
|
||||||
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
|
|
||||||
updateGpsMenuItem(isGpsStarted, isRecording);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the menu items with the icon specified.
|
* Updates the menu items with the icon specified.
|
||||||
*
|
*
|
||||||
* @param isGpsStarted true if gps is started
|
* @param isGpsStarted true if gps is started
|
||||||
* @param isRecording true if recording
|
* @param isRecording true if recording
|
||||||
*/
|
*/
|
||||||
|
//TODO Check if if can be avoided to call this outside of onGpsStatusChanged()
|
||||||
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
|
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
|
||||||
if (startGpsMenuItem != null) {
|
if (startGpsMenuItem != null) {
|
||||||
startGpsMenuItem.setVisible(!isRecording);
|
startGpsMenuItem.setVisible(!isRecording);
|
||||||
@@ -479,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (itemId == R.id.list_context_menu_edit) {
|
if (itemId == R.id.list_context_menu_edit) {
|
||||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||||
@@ -491,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
deleteTracks(trackIds);
|
deleteTracks(trackIds);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemId == R.id.list_context_menu_select_all) {
|
if (itemId == R.id.list_context_menu_select_all) {
|
||||||
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
||||||
viewBinding.trackList.setItemChecked(i, true);
|
viewBinding.trackList.setItemChecked(i, true);
|
||||||
@@ -558,25 +540,25 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
public void recordStart() {
|
public void recordStart() {
|
||||||
if (recordingStatus.getTrackId() == null) {
|
if (recordingStatus.getTrackId() == null) {
|
||||||
// Not recording -> Recording
|
// Not recording -> Recording
|
||||||
updateMenuItems(false, true);
|
updateGpsMenuItem(false, true);
|
||||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||||
startActivity(newIntent);
|
startActivity(newIntent);
|
||||||
} else if (recordingStatus.isPaused()) {
|
} else if (recordingStatus.isPaused()) {
|
||||||
// Paused -> Resume
|
// Paused -> Resume
|
||||||
updateMenuItems(false, true);
|
updateGpsMenuItem(false, true);
|
||||||
trackRecordingServiceConnection.resumeTrack();
|
trackRecordingServiceConnection.resumeTrack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recordPause() {
|
public void recordPause() {
|
||||||
updateMenuItems(false, true);
|
updateGpsMenuItem(false, true);
|
||||||
trackRecordingServiceConnection.pauseTrack();
|
trackRecordingServiceConnection.pauseTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recordStop() {
|
public void recordStop() {
|
||||||
updateMenuItems(false, false);
|
updateGpsMenuItem(false, false);
|
||||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,10 +567,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||||
recordingStatus = status;
|
recordingStatus = status;
|
||||||
|
|
||||||
trackController.update(recordingStatus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
|
|||||||
import com.google.android.material.tabs.TabLayoutMediator;
|
import com.google.android.material.tabs.TabLayoutMediator;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
||||||
@@ -42,6 +41,7 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
|
|||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||||
|
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||||
@@ -56,14 +56,13 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
|||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
|
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
|
||||||
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
public class TrackRecordedActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||||
|
|
||||||
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String VIEW_TRACK_ICON = "track_icon";
|
public static final String VIEW_TRACK_ICON = "track_icon";
|
||||||
|
|
||||||
public static final String EXTRA_TRACK_ID = "track_id";
|
public static final String EXTRA_TRACK_ID = "track_id";
|
||||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
|
||||||
|
|
||||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||||
|
|
||||||
@@ -248,9 +247,16 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
|||||||
return recordingStatus.getTrackId();
|
return recordingStatus.getTrackId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||||
|
super.onTrackDeleteStatus(deleteStatus);
|
||||||
|
if (deleteStatus.isDeleted(trackId)) {
|
||||||
|
runOnUiThread(this::finish);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTrackDeleted() {
|
protected void onDeleteConfirmed() {
|
||||||
runOnUiThread(this::finish);
|
runOnUiThread(this::finish);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,18 +270,6 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
|||||||
|
|
||||||
private void handleIntent(Intent intent) {
|
private void handleIntent(Intent intent) {
|
||||||
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||||
|
|
||||||
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
|
|
||||||
if (markerId != null) {
|
|
||||||
// Use the trackId from the marker
|
|
||||||
Marker marker = contentProviderUtils.getMarker(markerId);
|
|
||||||
if (marker == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trackId = marker.getTrackId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackId == null) {
|
if (trackId == null) {
|
||||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
|||||||
*/
|
*/
|
||||||
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
||||||
//This mode of communication should be removed anyhow.
|
//This mode of communication should be removed anyhow.
|
||||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
|
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, ControllerFragment.Callback {
|
||||||
|
|
||||||
public static final String EXTRA_TRACK_ID = "track_id";
|
public static final String EXTRA_TRACK_ID = "track_id";
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
private TrackDataHub trackDataHub;
|
private TrackDataHub trackDataHub;
|
||||||
private TrackController trackController;
|
|
||||||
|
|
||||||
private TrackRecordingBinding viewBinding;
|
private TrackRecordingBinding viewBinding;
|
||||||
|
|
||||||
@@ -90,8 +89,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
// A recording track is on.
|
// A recording track is on.
|
||||||
trackDataHub.loadTrack(trackId);
|
trackDataHub.loadTrack(trackId);
|
||||||
trackDataHub.setRecordingStatus(recordingStatus);
|
trackDataHub.setRecordingStatus(recordingStatus);
|
||||||
|
|
||||||
trackController.onResume(recordingStatus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -148,8 +145,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -214,7 +209,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
//TODO Pass recordingStatus directly to them
|
//TODO Pass recordingStatus directly to them
|
||||||
trackDataHub.loadTrack(trackId);
|
trackDataHub.loadTrack(trackId);
|
||||||
trackDataHub.setRecordingStatus(recordingStatus);
|
trackDataHub.setRecordingStatus(recordingStatus);
|
||||||
trackController.onResume(recordingStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -233,12 +227,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
trackController.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
@@ -406,7 +394,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||||
recordingStatus = status;
|
recordingStatus = status;
|
||||||
|
|
||||||
trackController.update(recordingStatus);
|
|
||||||
trackDataHub.setRecordingStatus(recordingStatus);
|
trackDataHub.setRecordingStatus(recordingStatus);
|
||||||
|
|
||||||
setLockscreenPolicy();
|
setLockscreenPolicy();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.graphics.Paint.Style;
|
|||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.ScaleGestureDetector;
|
import android.view.ScaleGestureDetector;
|
||||||
@@ -40,8 +41,8 @@ import androidx.core.view.GestureDetectorCompat;
|
|||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.MarkerDetailActivity;
|
import de.dennisguse.opentracks.MarkerDetailActivity;
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -114,7 +115,7 @@ public class ChartView extends View {
|
|||||||
private int effectiveWidth = 0;
|
private int effectiveWidth = 0;
|
||||||
private int effectiveHeight = 0;
|
private int effectiveHeight = 0;
|
||||||
|
|
||||||
private final boolean chartByDistance;
|
private boolean chartByDistance = false;
|
||||||
private boolean metricUnits = true;
|
private boolean metricUnits = true;
|
||||||
private boolean reportSpeed = true;
|
private boolean reportSpeed = true;
|
||||||
private boolean showPointer = false;
|
private boolean showPointer = false;
|
||||||
@@ -189,9 +190,9 @@ public class ChartView extends View {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public ChartView(Context context, final boolean chartByDistance) {
|
public ChartView(Context context, AttributeSet attributeSet) {
|
||||||
super(context);
|
super(context, attributeSet);
|
||||||
this.chartByDistance = chartByDistance;
|
|
||||||
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
||||||
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
||||||
|
|
||||||
@@ -366,6 +367,10 @@ public class ChartView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setChartByDistance(boolean chartByDistance) {
|
||||||
|
this.chartByDistance = chartByDistance;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getMetricUnits() {
|
public boolean getMetricUnits() {
|
||||||
return metricUnits;
|
return metricUnits;
|
||||||
}
|
}
|
||||||
@@ -424,7 +429,7 @@ public class ChartView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateDimensions();
|
updateDimensions();
|
||||||
updatePaths();
|
updateSeries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +476,7 @@ public class ChartView extends View {
|
|||||||
private void zoomIn() {
|
private void zoomIn() {
|
||||||
if (canZoomIn()) {
|
if (canZoomIn()) {
|
||||||
zoomLevel++;
|
zoomLevel++;
|
||||||
updatePaths();
|
updateSeries();
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,7 +491,7 @@ public class ChartView extends View {
|
|||||||
scrollX = maxWidth;
|
scrollX = maxWidth;
|
||||||
scrollTo(scrollX, 0);
|
scrollTo(scrollX, 0);
|
||||||
}
|
}
|
||||||
updatePaths();
|
updateSeries();
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,63 +858,49 @@ public class ChartView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates paths.
|
|
||||||
* The path needs to be updated any time after the data or the dimensions change.
|
* The path needs to be updated any time after the data or the dimensions change.
|
||||||
*/
|
*/
|
||||||
private void updatePaths() {
|
private void updateSeries() {
|
||||||
synchronized (chartPoints) {
|
synchronized (chartPoints) {
|
||||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
seriesList.stream().forEach(this::updateSerie);
|
||||||
chartValueSeries.getPath().reset();
|
|
||||||
}
|
|
||||||
drawPaths();
|
|
||||||
closePaths();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawPaths() {
|
private void updateSerie(ChartValueSeries series) {
|
||||||
boolean[] hasMoved = new boolean[seriesList.size()];
|
final int yCorner = topBorder + effectiveHeight;
|
||||||
|
final Path path = series.getPath();
|
||||||
|
|
||||||
for (ChartPoint dataPoint : chartPoints) {
|
boolean drawFirstPoint = false;
|
||||||
for (int i = 0; i < seriesList.size(); i++) {
|
path.rewind();
|
||||||
ChartValueSeries chartValueSeries = seriesList.get(i);
|
|
||||||
|
|
||||||
if (!chartValueSeries.isChartPointValid(dataPoint)) {
|
Iterator<ChartPoint> iterator = chartPoints.iterator();
|
||||||
continue;
|
while (iterator.hasNext()) {
|
||||||
}
|
ChartPoint point = iterator.next();
|
||||||
|
if (!series.isChartPointValid(point)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
|
double value = series.extractDataFromChartPoint(point);
|
||||||
Path path = chartValueSeries.getPath();
|
int x = getX(point.getTimeOrDistance());
|
||||||
int x = getX(dataPoint.getTimeOrDistance());
|
int y = getY(series, value);
|
||||||
int y = getY(chartValueSeries, value);
|
|
||||||
if (!hasMoved[i]) {
|
// start from lower left corner
|
||||||
hasMoved[i] = true;
|
if (!drawFirstPoint) {
|
||||||
path.moveTo(x, y);
|
path.moveTo(x, yCorner);
|
||||||
} else {
|
drawFirstPoint = true;
|
||||||
path.lineTo(x, y);
|
}
|
||||||
}
|
|
||||||
|
// draw graph
|
||||||
|
path.lineTo(x, y);
|
||||||
|
|
||||||
|
// last point: move to lower right
|
||||||
|
if (!iterator.hasNext()) {
|
||||||
|
path.lineTo(x, yCorner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void closePaths() {
|
// back to lower left corner
|
||||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
path.close();
|
||||||
Optional<ChartPoint> firstValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
|
|
||||||
.findFirst();
|
|
||||||
Optional<ChartPoint> lastValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
|
|
||||||
.reduce((first, second) -> second);
|
|
||||||
|
|
||||||
if (firstValid.isPresent() && lastValid.isPresent()) {
|
|
||||||
Path path = chartValueSeries.getPath();
|
|
||||||
|
|
||||||
int yCorner = topBorder + effectiveHeight;
|
|
||||||
|
|
||||||
path.lineTo(getX(lastValid.get().getTimeOrDistance()), yCorner);
|
|
||||||
path.lineTo(getX(firstValid.get().getTimeOrDistance()), yCorner);
|
|
||||||
|
|
||||||
double value = chartValueSeries.extractDataFromChartPoint(firstValid.get());
|
|
||||||
path.lineTo(getX(firstValid.get().getTimeOrDistance()), getY(chartValueSeries, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -961,7 +952,7 @@ public class ChartView extends View {
|
|||||||
width = newWidth;
|
width = newWidth;
|
||||||
height = newHeight;
|
height = newHeight;
|
||||||
updateEffectiveDimensions();
|
updateEffectiveDimensions();
|
||||||
updatePaths();
|
updateSeries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Altitude;
|
import de.dennisguse.opentracks.content.data.Altitude;
|
||||||
@@ -71,13 +72,13 @@ public class TrackDataHub {
|
|||||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final TrackDataManager trackDataManager;
|
private final Set<TrackDataListener> listeners;
|
||||||
private final ContentProviderUtils contentProviderUtils;
|
private final ContentProviderUtils contentProviderUtils;
|
||||||
private final int targetNumPoints;
|
private final int targetNumPoints;
|
||||||
|
|
||||||
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
||||||
|
|
||||||
private boolean started;
|
//TODO Check if this is needed.
|
||||||
private HandlerThread handlerThread;
|
private HandlerThread handlerThread;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
|
||||||
@@ -99,24 +100,23 @@ public class TrackDataHub {
|
|||||||
private ContentObserver trackPointsTableObserver;
|
private ContentObserver trackPointsTableObserver;
|
||||||
|
|
||||||
public TrackDataHub(Context context) {
|
public TrackDataHub(Context context) {
|
||||||
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
this(context, new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
private TrackDataHub(Context context, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.trackDataManager = trackDataManager;
|
this.listeners = new HashSet<>();
|
||||||
this.contentProviderUtils = contentProviderUtils;
|
this.contentProviderUtils = contentProviderUtils;
|
||||||
this.targetNumPoints = targetNumPoints;
|
this.targetNumPoints = targetNumPoints;
|
||||||
resetSamplingState();
|
resetSamplingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
if (started) {
|
if (isStarted()) {
|
||||||
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
started = true;
|
|
||||||
handlerThread = new HandlerThread(TAG);
|
handlerThread = new HandlerThread(TAG);
|
||||||
handlerThread.start();
|
handlerThread.start();
|
||||||
handler = new Handler(handlerThread.getLooper());
|
handler = new Handler(handlerThread.getLooper());
|
||||||
@@ -126,7 +126,7 @@ public class TrackDataHub {
|
|||||||
tracksTableObserver = new ContentObserver(handler) {
|
tracksTableObserver = new ContentObserver(handler) {
|
||||||
@Override
|
@Override
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
notifyTracksTableUpdate(listeners);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
|
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
|
||||||
@@ -134,7 +134,7 @@ public class TrackDataHub {
|
|||||||
markersTableObserver = new ContentObserver(handler) {
|
markersTableObserver = new ContentObserver(handler) {
|
||||||
@Override
|
@Override
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
notifyMarkersTableUpdate(listeners);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
|
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
|
||||||
@@ -142,21 +142,18 @@ public class TrackDataHub {
|
|||||||
trackPointsTableObserver = new ContentObserver(handler) {
|
trackPointsTableObserver = new ContentObserver(handler) {
|
||||||
@Override
|
@Override
|
||||||
public void onChange(boolean selfChange) {
|
public void onChange(boolean selfChange) {
|
||||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
notifyTrackPointsTableUpdate(true, listeners);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
|
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (!started) {
|
if (!isStarted()) {
|
||||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
started = false;
|
|
||||||
trackStatisticsUpdater = null;
|
|
||||||
|
|
||||||
//Unregister listeners
|
//Unregister listeners
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
contentResolver.unregisterContentObserver(tracksTableObserver);
|
contentResolver.unregisterContentObserver(tracksTableObserver);
|
||||||
@@ -168,6 +165,7 @@ public class TrackDataHub {
|
|||||||
handlerThread = null;
|
handlerThread = null;
|
||||||
}
|
}
|
||||||
handler = null;
|
handler = null;
|
||||||
|
trackStatisticsUpdater = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadTrack(final @NonNull Track.Id trackId) {
|
public void loadTrack(final @NonNull Track.Id trackId) {
|
||||||
@@ -186,10 +184,10 @@ public class TrackDataHub {
|
|||||||
*
|
*
|
||||||
* @param trackDataListener the track data listener
|
* @param trackDataListener the track data listener
|
||||||
*/
|
*/
|
||||||
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean markersTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
|
public void registerTrackDataListener(final TrackDataListener trackDataListener) {
|
||||||
handler.post(() -> {
|
handler.post(() -> {
|
||||||
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
|
listeners.add(trackDataListener);
|
||||||
if (started) {
|
if (isStarted()) {
|
||||||
loadDataForListener(trackDataListener);
|
loadDataForListener(trackDataListener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -201,7 +199,7 @@ public class TrackDataHub {
|
|||||||
* @param trackDataListener the track data listener
|
* @param trackDataListener the track data listener
|
||||||
*/
|
*/
|
||||||
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
|
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
|
||||||
handler.post(() -> trackDataManager.unregisterTrackDataListener(trackDataListener));
|
handler.post(() -> listeners.remove(trackDataListener));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,17 +221,17 @@ public class TrackDataHub {
|
|||||||
*/
|
*/
|
||||||
private void loadDataForAll() {
|
private void loadDataForAll() {
|
||||||
resetSamplingState();
|
resetSamplingState();
|
||||||
if (!trackDataManager.hasListeners()) {
|
if (listeners.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
notifyTracksTableUpdate(listeners);
|
||||||
|
|
||||||
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
|
for (TrackDataListener listener : listeners) {
|
||||||
listener.clearTrackPoints();
|
listener.clearTrackPoints();
|
||||||
}
|
}
|
||||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
notifyTrackPointsTableUpdate(true, listeners);
|
||||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
notifyMarkersTableUpdate(listeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,25 +242,19 @@ public class TrackDataHub {
|
|||||||
private void loadDataForListener(TrackDataListener trackDataListener) {
|
private void loadDataForListener(TrackDataListener trackDataListener) {
|
||||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||||
|
|
||||||
if (trackDataManager.listensForTracks(trackDataListener)) {
|
//Track
|
||||||
notifyTracksTableUpdate(trackDataListeners);
|
notifyTracksTableUpdate(trackDataListeners);
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
|
//TrackPoints
|
||||||
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
|
trackDataListener.clearTrackPoints();
|
||||||
if (hasSampledIn || hasSampledOut) {
|
boolean isOnlyListener = listeners.size() == 1;
|
||||||
trackDataListener.clearTrackPoints();
|
if (isOnlyListener) {
|
||||||
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
|
resetSamplingState();
|
||||||
if (isOnlyListener) {
|
|
||||||
resetSamplingState();
|
|
||||||
}
|
|
||||||
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.emptySet();
|
|
||||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
|
|
||||||
}
|
}
|
||||||
|
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners);
|
||||||
|
|
||||||
if (trackDataManager.listensForMarkers(trackDataListener)) {
|
//Markers
|
||||||
notifyMarkersTableUpdate(trackDataListeners);
|
notifyMarkersTableUpdate(trackDataListeners);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,11 +307,9 @@ public class TrackDataHub {
|
|||||||
* Notifies track points table update; to be run in the {@link #handler} thread.
|
* Notifies track points table update; to be run in the {@link #handler} thread.
|
||||||
*
|
*
|
||||||
* @param updateSamplingState true to update the sampling state
|
* @param updateSamplingState true to update the sampling state
|
||||||
* @param sampledInListeners the sampled-in listeners
|
|
||||||
* @param sampledOutListeners the sampled-out listeners
|
|
||||||
*/
|
*/
|
||||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
|
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> listeners) {
|
||||||
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
|
if (listeners.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +317,7 @@ public class TrackDataHub {
|
|||||||
// Reload and resample the track at a lower frequency.
|
// Reload and resample the track at a lower frequency.
|
||||||
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
|
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
|
||||||
resetSamplingState();
|
resetSamplingState();
|
||||||
for (TrackDataListener listener : sampledInListeners) {
|
for (TrackDataListener listener : listeners) {
|
||||||
listener.clearTrackPoints();
|
listener.clearTrackPoints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +345,13 @@ public class TrackDataHub {
|
|||||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||||
|
|
||||||
while (trackPointIterator.hasNext()) {
|
while (trackPointIterator.hasNext()) {
|
||||||
|
if (!isStarted()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
|
||||||
|
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater;
|
||||||
|
|
||||||
trackPoint = trackPointIterator.next();
|
trackPoint = trackPointIterator.next();
|
||||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||||
|
|
||||||
@@ -374,16 +371,16 @@ public class TrackDataHub {
|
|||||||
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
|
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
currentUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||||
|
|
||||||
// Also include the last point if the selected track is not recording.
|
// Also include the last point if the selected track is not recording.
|
||||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
for (TrackDataListener trackDataListener : listeners) {
|
||||||
trackDataListener.onSampledInTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics(), trackStatisticsUpdater.getSmoothedSpeed(), trackStatisticsUpdater.getSmoothedAltitude());
|
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
for (TrackDataListener trackDataListener : listeners) {
|
||||||
trackDataListener.onSampledOutTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics());
|
trackDataListener.onSampledOutTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,11 +398,7 @@ public class TrackDataHub {
|
|||||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackPoint != null) {
|
listeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
|
||||||
for (TrackDataListener listener : sampledInListeners) {
|
|
||||||
listener.onNewTrackPointsDone(trackPoint, trackStatisticsUpdater.getTrackStatistics());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void correctAltitude(TrackPoint trackPoint) {
|
private void correctAltitude(TrackPoint trackPoint) {
|
||||||
@@ -436,6 +429,10 @@ public class TrackDataHub {
|
|||||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isStarted() {
|
||||||
|
return handlerThread != null;
|
||||||
|
}
|
||||||
|
|
||||||
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
|
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
|
||||||
this.recordingStatus = recordingStatus;
|
this.recordingStatus = recordingStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,26 +62,28 @@ public interface TrackDataListener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when finish sending new track points.
|
* Called when finish sending new track points.
|
||||||
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint, TrackStatistics, Speed, double)} and {@link #onSampledOutTrackPoint(TrackPoint, TrackStatistics)}.
|
|
||||||
*/
|
*/
|
||||||
default void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint, @NonNull TrackStatistics trackStatistics) {
|
default void onNewTrackPointsDone() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called to clear previously sent markers.
|
* Called to clear previously sent markers.
|
||||||
*/
|
*/
|
||||||
void clearMarkers();
|
default void clearMarkers() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a new marker is read.
|
* Called when a new marker is read.
|
||||||
*
|
*
|
||||||
* @param marker the marker
|
* @param marker the marker
|
||||||
*/
|
*/
|
||||||
void onNewMarker(Marker marker);
|
default void onNewMarker(@NonNull Marker marker) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when finish sending new markers.
|
* Called when finish sending new markers.
|
||||||
* This gets called after every batch of calls to {@link #clearMarkers()} and {@link #onNewMarker(Marker)}.
|
* This gets called after every batch of calls to {@link #clearMarkers()} and {@link #onNewMarker(Marker)}.
|
||||||
*/
|
*/
|
||||||
void onNewMarkersDone();
|
default void onNewMarkersDone() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks.content;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
|
|
||||||
*
|
|
||||||
* @author Rodrigo Damazio
|
|
||||||
*/
|
|
||||||
class TrackDataManager {
|
|
||||||
|
|
||||||
private static final String TAG = TrackDataManager.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
|
|
||||||
private final Set<TrackDataListener> dataListenerMarkers = new HashSet<>();
|
|
||||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
|
|
||||||
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
|
|
||||||
|
|
||||||
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean markersTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
|
|
||||||
if (tracksTable) dataListenerTracks.add(trackDataListener);
|
|
||||||
if (markersTable) dataListenerMarkers.add(trackDataListener);
|
|
||||||
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
|
|
||||||
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
|
|
||||||
dataListenerTracks.remove(trackDataListener);
|
|
||||||
dataListenerMarkers.remove(trackDataListener);
|
|
||||||
dataListenerTrackPoints_SampledIn.remove(trackDataListener);
|
|
||||||
dataListenerTrackPoints_SampledOut.remove(trackDataListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasListeners() {
|
|
||||||
return dataListenerTracks.size() + dataListenerMarkers.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getNumberOfListeners() {
|
|
||||||
Set<TrackDataListener> listener = new HashSet<>();
|
|
||||||
listener.addAll(dataListenerTracks);
|
|
||||||
listener.addAll(dataListenerMarkers);
|
|
||||||
listener.addAll(dataListenerTrackPoints_SampledIn);
|
|
||||||
listener.addAll(dataListenerTrackPoints_SampledOut);
|
|
||||||
|
|
||||||
return listener.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean listensForTracks(TrackDataListener listener) {
|
|
||||||
return dataListenerTracks.contains(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean listensForMarkers(TrackDataListener listener) {
|
|
||||||
return dataListenerMarkers.contains(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
|
|
||||||
return dataListenerTrackPoints_SampledIn.contains(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
|
|
||||||
return dataListenerTrackPoints_SampledOut.contains(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<TrackDataListener> getListenerTracks() {
|
|
||||||
return dataListenerTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<TrackDataListener> getListenerMarkers() {
|
|
||||||
return dataListenerMarkers;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
|
|
||||||
return dataListenerTrackPoints_SampledIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
|
|
||||||
return dataListenerTrackPoints_SampledOut;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,6 +24,10 @@ public class Distance {
|
|||||||
return of(distance_km * UnitConversions.KM_TO_M);
|
return of(distance_km * UnitConversions.KM_TO_M);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Distance ofMM(double distance_mm) {
|
||||||
|
return of(distance_mm * UnitConversions.MM_TO_M);
|
||||||
|
}
|
||||||
|
|
||||||
public static Distance one(boolean metricUnit) {
|
public static Distance one(boolean metricUnit) {
|
||||||
if (metricUnit) {
|
if (metricUnit) {
|
||||||
return Distance.ofKilometer(1);
|
return Distance.ofKilometer(1);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public final class Marker {
|
|||||||
private String description = "";
|
private String description = "";
|
||||||
private String category = "";
|
private String category = "";
|
||||||
private String icon = "";
|
private String icon = "";
|
||||||
private final Track.Id trackId;
|
private Track.Id trackId;
|
||||||
|
|
||||||
private final Instant time;
|
private final Instant time;
|
||||||
private Double latitude;
|
private Double latitude;
|
||||||
@@ -74,16 +74,13 @@ public final class Marker {
|
|||||||
if (!trackPoint.hasLocation())
|
if (!trackPoint.hasLocation())
|
||||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||||
|
|
||||||
this.latitude = trackPoint.getLatitude();
|
setTrackPoint(trackPoint);
|
||||||
this.longitude = trackPoint.getLongitude();
|
|
||||||
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
|
||||||
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
|
|
||||||
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
|
||||||
|
|
||||||
this.length = Distance.of(0); //TODO Not cool!
|
this.length = Distance.of(0); //TODO Not cool!
|
||||||
this.duration = Duration.ofMillis(0); //TODO Not cool!
|
this.duration = Duration.ofMillis(0); //TODO Not cool!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
|
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
|
||||||
this(trackId, trackPoint);
|
this(trackId, trackPoint);
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -95,6 +92,15 @@ public final class Marker {
|
|||||||
this.photoUrl = photoUrl;
|
this.photoUrl = photoUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Is somehow part of the initialization process. Can we at least limit visibility?
|
||||||
|
public void setTrackPoint(TrackPoint trackPoint) {
|
||||||
|
this.latitude = trackPoint.getLatitude();
|
||||||
|
this.longitude = trackPoint.getLongitude();
|
||||||
|
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
|
||||||
|
if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
|
||||||
|
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* May be null if the it was not loaded from the database.
|
* May be null if the it was not loaded from the database.
|
||||||
*/
|
*/
|
||||||
@@ -143,16 +149,19 @@ public final class Marker {
|
|||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Track.Id getTrackId() {
|
public Track.Id getTrackId() {
|
||||||
return trackId;
|
return trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public void setTrackId(@NonNull Track.Id trackId) {
|
||||||
|
this.trackId = trackId;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasLocation() {
|
public boolean hasLocation() {
|
||||||
return latitude != null || longitude != null;
|
return latitude != null || longitude != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Location getLocation() {
|
public Location getLocation() {
|
||||||
Location location = new Location("");
|
Location location = new Location("");
|
||||||
location.setTime(time.toEpochMilli());
|
location.setTime(time.toEpochMilli());
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
|||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
|
//TODO Do not default initialize attributes; might be confusing for debugging
|
||||||
public class Track {
|
public class Track {
|
||||||
|
|
||||||
private Track.Id id;
|
private Track.Id id;
|
||||||
@@ -46,9 +47,6 @@ public class Track {
|
|||||||
|
|
||||||
private TrackStatistics trackStatistics = new TrackStatistics();
|
private TrackStatistics trackStatistics = new TrackStatistics();
|
||||||
|
|
||||||
public Track() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* May be null if the track was not loaded from the database.
|
* May be null if the track was not loaded from the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class TrackPoint {
|
|||||||
private Altitude altitude;
|
private Altitude altitude;
|
||||||
private Speed speed;
|
private Speed speed;
|
||||||
private Float bearing;
|
private Float bearing;
|
||||||
private Distance sensorDistance_m;
|
private Distance sensorDistance;
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
||||||
@@ -99,7 +99,11 @@ public class TrackPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public TrackPoint(@NonNull Location location) {
|
public TrackPoint(@NonNull Location location) {
|
||||||
this(Type.TRACKPOINT);
|
this(Type.TRACKPOINT, location, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
|
||||||
|
this(type);
|
||||||
|
|
||||||
this.latitude = location.getLatitude();
|
this.latitude = location.getLatitude();
|
||||||
this.longitude = location.getLongitude();
|
this.longitude = location.getLongitude();
|
||||||
@@ -107,7 +111,9 @@ public class TrackPoint {
|
|||||||
this.speed = Speed.of(location.getSpeed());
|
this.speed = Speed.of(location.getSpeed());
|
||||||
this.accuracy = location.getAccuracy();
|
this.accuracy = location.getAccuracy();
|
||||||
|
|
||||||
setTime(Instant.now());
|
//TODO Should we copy the bearing?
|
||||||
|
|
||||||
|
setTime(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackPoint(@NonNull Type type, Instant time) {
|
public TrackPoint(@NonNull Type type, Instant time) {
|
||||||
@@ -198,6 +204,7 @@ public class TrackPoint {
|
|||||||
this.longitude = longitude;
|
this.longitude = longitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||||
@NonNull
|
@NonNull
|
||||||
public Location getLocation() {
|
public Location getLocation() {
|
||||||
Location location = new Location("");
|
Location location = new Location("");
|
||||||
@@ -314,10 +321,14 @@ public class TrackPoint {
|
|||||||
this.accuracy = horizontalAccuracy;
|
this.accuracy = horizontalAccuracy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||||
if (hasSensorDistance()) {
|
if (hasSensorDistance()) {
|
||||||
return getSensorDistance();
|
return getSensorDistance();
|
||||||
}
|
}
|
||||||
|
if (!(hasLocation() && previous.hasLocation())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return Distance.of(getLocation().distanceTo(previous.getLocation()));
|
return Distance.of(getLocation().distanceTo(previous.getLocation()));
|
||||||
}
|
}
|
||||||
@@ -326,25 +337,27 @@ public class TrackPoint {
|
|||||||
return hasAccuracy() && accuracy < poorAccuracy;
|
return hasAccuracy() && accuracy < poorAccuracy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||||
public float bearingTo(@NonNull TrackPoint dest) {
|
public float bearingTo(@NonNull TrackPoint dest) {
|
||||||
return getLocation().bearingTo(dest.getLocation());
|
return getLocation().bearingTo(dest.getLocation());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Bearing requires a location; what do we do if we don't have any?
|
||||||
public float bearingTo(@NonNull Location dest) {
|
public float bearingTo(@NonNull Location dest) {
|
||||||
return getLocation().bearingTo(dest);
|
return getLocation().bearingTo(dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sensor data
|
// Sensor data
|
||||||
public boolean hasSensorDistance() {
|
public boolean hasSensorDistance() {
|
||||||
return sensorDistance_m != null;
|
return sensorDistance != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Distance getSensorDistance() {
|
public Distance getSensorDistance() {
|
||||||
return sensorDistance_m;
|
return sensorDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSensorDistance(Distance distance_m) {
|
public void setSensorDistance(Distance distance_m) {
|
||||||
this.sensorDistance_m = distance_m;
|
this.sensorDistance = distance_m;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSensorData() {
|
public boolean hasSensorData() {
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import de.dennisguse.opentracks.util.UUIDUtils;
|
|||||||
public class ContentProviderUtils {
|
public class ContentProviderUtils {
|
||||||
|
|
||||||
private static final String TAG = ContentProviderUtils.class.getSimpleName();
|
private static final String TAG = ContentProviderUtils.class.getSimpleName();
|
||||||
private static final int MAX_LATITUDE = 90000000;
|
|
||||||
|
|
||||||
// The authority (the first part of the URI) for the app's content provider.
|
// The authority (the first part of the URI) for the app's content provider.
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -161,8 +160,10 @@ public class ContentProviderUtils {
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void deleteAllTracks(Context context) {
|
public void deleteAllTracks(Context context) {
|
||||||
|
//TODO Both calls should not be necessary
|
||||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
|
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
|
||||||
contentResolver.delete(MarkerColumns.CONTENT_URI, null, null);
|
contentResolver.delete(MarkerColumns.CONTENT_URI, null, null);
|
||||||
|
|
||||||
// Delete tracks last since it triggers a database vaccum call
|
// Delete tracks last since it triggers a database vaccum call
|
||||||
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
|
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||||
|
|
||||||
@@ -190,6 +191,7 @@ public class ContentProviderUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO Only use for tests; also move to tests.
|
//TODO Only use for tests; also move to tests.
|
||||||
|
@VisibleForTesting
|
||||||
public List<Track> getTracks() {
|
public List<Track> getTracks() {
|
||||||
ArrayList<Track> tracks = new ArrayList<>();
|
ArrayList<Track> tracks = new ArrayList<>();
|
||||||
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
|
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
|
||||||
@@ -203,13 +205,7 @@ public class ContentProviderUtils {
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Track getTrack(@NonNull Track.Id trackId) {
|
||||||
* @param trackId the track id.
|
|
||||||
*/
|
|
||||||
public Track getTrack(Track.Id trackId) {
|
|
||||||
if (trackId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
|
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
return createTrack(cursor);
|
return createTrack(cursor);
|
||||||
@@ -218,9 +214,6 @@ public class ContentProviderUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param trackUUID the track uuid.
|
|
||||||
*/
|
|
||||||
public Track getTrack(@NonNull UUID trackUUID) {
|
public Track getTrack(@NonNull UUID trackUUID) {
|
||||||
String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
|
String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
|
||||||
try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) {
|
try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) {
|
||||||
@@ -250,6 +243,7 @@ public class ContentProviderUtils {
|
|||||||
* @param track the track
|
* @param track the track
|
||||||
* @return the content provider URI of the inserted track.
|
* @return the content provider URI of the inserted track.
|
||||||
*/
|
*/
|
||||||
|
///TODO Return Track.Id
|
||||||
public Uri insertTrack(Track track) {
|
public Uri insertTrack(Track track) {
|
||||||
return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
|
return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track));
|
||||||
}
|
}
|
||||||
@@ -576,6 +570,7 @@ public class ContentProviderUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO Only used for file import; might be better to replace it.
|
//TODO Only used for file import; might be better to replace it.
|
||||||
|
//TODO Rename to bulkInsert
|
||||||
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
|
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
|
||||||
ContentValues[] values = new ContentValues[trackPoints.size()];
|
ContentValues[] values = new ContentValues[trackPoints.size()];
|
||||||
for (int i = 0; i < trackPoints.size(); i++) {
|
for (int i = 0; i < trackPoints.size(); i++) {
|
||||||
@@ -584,6 +579,15 @@ public class ContentProviderUtils {
|
|||||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Set trackId in this method.
|
||||||
|
public int bulkInsertMarkers(List<Marker> markers, Track.Id trackId) {
|
||||||
|
ContentValues[] values = new ContentValues[markers.size()];
|
||||||
|
for (int i = 0; i < markers.size(); i++) {
|
||||||
|
values[i] = createContentValues(markers.get(i));
|
||||||
|
}
|
||||||
|
return contentResolver.bulkInsert(MarkerColumns.CONTENT_URI, values);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the last location id for a track.
|
* Gets the last location id for a track.
|
||||||
* Returns -1L if it doesn't exist.
|
* Returns -1L if it doesn't exist.
|
||||||
@@ -609,6 +613,7 @@ public class ContentProviderUtils {
|
|||||||
* @param location the location
|
* @param location the location
|
||||||
* @return trackPoint id if the location is in the track. -1L otherwise.
|
* @return trackPoint id if the location is in the track. -1L otherwise.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
|
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
|
||||||
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
|
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
|
||||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
|
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
|
||||||
@@ -740,6 +745,7 @@ public class ContentProviderUtils {
|
|||||||
return new TrackPointIterator(this, trackId, startTrackPointId);
|
return new TrackPointIterator(this, trackId, startTrackPointId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
|
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
|
||||||
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
|
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public final class SensorDataCycling {
|
|||||||
return wheelRevolutionsTime;
|
return wheelRevolutionsTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
|
public void compute(DistanceSpeed previous, Distance wheelCircumference) {
|
||||||
if (hasData() && previous != null && previous.hasData()) {
|
if (hasData() && previous != null && previous.hasData()) {
|
||||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||||
@@ -134,7 +134,7 @@ public final class SensorDataCycling {
|
|||||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||||
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
|
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
|
||||||
|
|
||||||
Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
|
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||||
Distance distanceOverall = distance;
|
Distance distanceOverall = distance;
|
||||||
if (previous.hasValue()) {
|
if (previous.hasValue()) {
|
||||||
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
||||||
|
|||||||
@@ -16,28 +16,25 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks.fragments;
|
package de.dennisguse.opentracks.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewGroup.LayoutParams;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||||
import de.dennisguse.opentracks.chart.ChartView;
|
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
@@ -68,7 +65,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private Distance recordingDistanceInterval;
|
|
||||||
|
|
||||||
private TrackDataHub trackDataHub;
|
private TrackDataHub trackDataHub;
|
||||||
|
|
||||||
@@ -80,40 +76,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
private boolean chartByDistance;
|
private boolean chartByDistance;
|
||||||
|
|
||||||
private ChartBinding viewBinding;
|
private ChartBinding viewBinding;
|
||||||
// UI elements
|
|
||||||
private ChartView chartView;
|
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||||
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||||
if (metricUnits != chartView.getMetricUnits()) {
|
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
|
||||||
chartView.setMetricUnits(metricUnits);
|
viewBinding.chartView.setMetricUnits(metricUnits);
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
chartView.requestLayout();
|
viewBinding.chartView.requestLayout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||||
if (reportSpeed != chartView.getReportSpeed()) {
|
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||||
chartView.setReportSpeed(reportSpeed);
|
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||||
chartView.applyReportSpeed();
|
viewBinding.chartView.applyReportSpeed();
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
chartView.requestLayout();
|
viewBinding.chartView.requestLayout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
|
|
||||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, getContext());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,12 +113,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
private final Runnable updateChart = new Runnable() {
|
private final Runnable updateChart = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (!isResumed() || trackDataHub == null) {
|
if (!isResumed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chartView.setShowPointer(isSelectedTrackRecording());
|
viewBinding.chartView.setShowPointer(isSelectedTrackRecording());
|
||||||
chartView.invalidate();
|
viewBinding.chartView.invalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,27 +128,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true);
|
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true);
|
||||||
|
|
||||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
|
|
||||||
|
|
||||||
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
|
|
||||||
chartView = new ChartView(getContext(), chartByDistance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
viewBinding = ChartBinding.inflate(inflater, container, false);
|
viewBinding = ChartBinding.inflate(inflater, container, false);
|
||||||
|
viewBinding.chartView.setChartByDistance(chartByDistance);
|
||||||
return viewBinding.getRoot();
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
|
|
||||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
|
||||||
viewBinding.chartViewLayout.addView(chartView, layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -178,13 +156,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
viewBinding.chartViewLayout.removeView(chartView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
@@ -200,16 +171,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onTrackUpdated(Track track) {
|
public void onTrackUpdated(Track track) {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
if (track == null || track.getTrackStatistics() == null) {
|
if (track == null) {
|
||||||
category = "";
|
category = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
category = track.getCategory();
|
category = track.getCategory();
|
||||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
|
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
|
||||||
if (reportSpeed != chartView.getReportSpeed()) {
|
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||||
chartView.setReportSpeed(reportSpeed);
|
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||||
chartView.applyReportSpeed();
|
viewBinding.chartView.applyReportSpeed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,10 +189,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
public void clearTrackPoints() {
|
public void clearTrackPoints() {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
pendingPoints.clear();
|
pendingPoints.clear();
|
||||||
chartView.reset();
|
viewBinding.chartView.reset();
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
chartView.resetScroll();
|
viewBinding.chartView.resetScroll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,15 +200,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
|
|
||||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
|
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, chartView.getMetricUnits());
|
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, viewBinding.chartView.getMetricUnits());
|
||||||
pendingPoints.add(point);
|
pendingPoints.add(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
|
public void onNewTrackPointsDone() {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
chartView.addChartPoints(pendingPoints);
|
//Avoid ConcurrentModificationException exception
|
||||||
|
viewBinding.chartView.addChartPoints(Collections.unmodifiableList(pendingPoints));
|
||||||
pendingPoints.clear();
|
pendingPoints.clear();
|
||||||
runOnUiThread(updateChart);
|
runOnUiThread(updateChart);
|
||||||
}
|
}
|
||||||
@@ -246,14 +218,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
@Override
|
@Override
|
||||||
public void clearMarkers() {
|
public void clearMarkers() {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
chartView.clearMarker();
|
viewBinding.chartView.clearMarker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewMarker(Marker marker) {
|
public void onNewMarker(@NonNull Marker marker) {
|
||||||
if (isResumed() && marker != null) {
|
if (isResumed()) {
|
||||||
chartView.addMarker(marker);
|
viewBinding.chartView.addMarker(marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +240,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
* Checks the chart settings.
|
* Checks the chart settings.
|
||||||
*/
|
*/
|
||||||
private void checkChartSettings() {
|
private void checkChartSettings() {
|
||||||
boolean needUpdate = chartView.applyReportSpeed();
|
boolean needUpdate = viewBinding.chartView.applyReportSpeed();
|
||||||
if (needUpdate) {
|
if (needUpdate) {
|
||||||
chartView.postInvalidate();
|
viewBinding.chartView.postInvalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +252,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
*/
|
*/
|
||||||
private synchronized void resumeTrackDataHub() {
|
private synchronized void resumeTrackDataHub() {
|
||||||
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||||
trackDataHub.registerTrackDataListener(this, true, true, true, true);
|
trackDataHub.registerTrackDataListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,14 +280,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
* @param runnable the runnable
|
* @param runnable the runnable
|
||||||
*/
|
*/
|
||||||
private void runOnUiThread(Runnable runnable) {
|
private void runOnUiThread(Runnable runnable) {
|
||||||
FragmentActivity fragmentActivity = getActivity();
|
Activity fragmentActivity = getActivity();
|
||||||
if (fragmentActivity != null) {
|
if (fragmentActivity != null) {
|
||||||
fragmentActivity.runOnUiThread(runnable);
|
fragmentActivity.runOnUiThread(runnable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void setChartView(ChartView view) {
|
|
||||||
chartView = view;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
|||||||
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
@@ -195,7 +194,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
|||||||
*/
|
*/
|
||||||
private synchronized void resumeTrackDataHub() {
|
private synchronized void resumeTrackDataHub() {
|
||||||
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||||
trackDataHub.registerTrackDataListener(this, true, false, true, true);
|
trackDataHub.registerTrackDataListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,27 +244,12 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
|
public void onNewTrackPointsDone() {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
runOnUiThread(viewModel::onNewTrackPoints);
|
runOnUiThread(viewModel::onNewTrackPoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearMarkers() {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewMarker(Marker marker) {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewMarkersDone() {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs a runnable on the UI thread.
|
* Runs a runnable on the UI thread.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.fragments;
|
|||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -22,10 +21,6 @@ import java.util.List;
|
|||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||||
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
|
||||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
@@ -34,7 +29,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
|||||||
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
@@ -46,25 +40,17 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
|||||||
* @author Sandor Dornbush
|
* @author Sandor Dornbush
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
|
public class StatisticsRecordingFragment extends Fragment {
|
||||||
|
|
||||||
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
|
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
|
||||||
|
|
||||||
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
|
||||||
|
|
||||||
public static Fragment newInstance() {
|
public static Fragment newInstance() {
|
||||||
return new StatisticsRecordingFragment();
|
return new StatisticsRecordingFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TrackDataHub trackDataHub;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
|
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
|
||||||
private Handler handlerUpdateUI;
|
private TrackPoint latestTrackPoint;
|
||||||
|
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
|
||||||
private TrackPoint lastTrackPoint;
|
|
||||||
|
|
||||||
@Nullable // Lazily loaded.
|
|
||||||
private Track track;
|
|
||||||
|
|
||||||
private StatisticsRecordingBinding viewBinding;
|
private StatisticsRecordingBinding viewBinding;
|
||||||
private SensorsAdapter sensorsAdapter;
|
private SensorsAdapter sensorsAdapter;
|
||||||
@@ -83,9 +69,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && track != null) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && recordingData != null) {
|
||||||
updateUInecessary = true;
|
updateUInecessary = true;
|
||||||
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory());
|
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), recordingData.getTrackCategory());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
|
||||||
@@ -99,39 +85,29 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (key != null && updateUInecessary && isResumed()) {
|
if (key != null && updateUInecessary && isResumed()) {
|
||||||
getActivity().runOnUiThread(() -> {
|
getActivity().runOnUiThread(this::updateUI);
|
||||||
if (isResumed()) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final Runnable updateUIeachSecond = new Runnable() {
|
private final Runnable bindChangedCallback = new Runnable() {
|
||||||
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (isResumed() && isSelectedTrackRecording()) {
|
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||||
if (!isSelectedTrackPaused() && track != null) {
|
if (service == null) {
|
||||||
synchronized (this) {
|
Log.w(TAG, "could not get TrackRecordingService");
|
||||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
|
return;
|
||||||
lastTrackPoint = null;
|
|
||||||
setLocationValues();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateTotalTime();
|
|
||||||
updateSensorDataUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.getRecordingDataObservable()
|
||||||
|
.observe(StatisticsRecordingFragment.this, recordingData -> onRecordingDataChanged(recordingData));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
handlerUpdateUI = new Handler();
|
|
||||||
|
|
||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||||
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||||
|
|
||||||
sensorsAdapter = new SensorsAdapter(getContext());
|
sensorsAdapter = new SensorsAdapter(getContext());
|
||||||
}
|
}
|
||||||
@@ -139,7 +115,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
||||||
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(track != null ? track.getCategory() : "")); //TODO "" would be useless as Track was not yet loaded.
|
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(recordingData.getTrackCategory()));
|
||||||
|
|
||||||
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||||
@@ -151,25 +127,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
resumeTrackDataHub();
|
|
||||||
|
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||||
|
|
||||||
trackRecordingServiceConnection.startConnection(getContext());
|
trackRecordingServiceConnection.startConnection(getContext());
|
||||||
|
|
||||||
updateUIeachSecond.run();
|
|
||||||
handlerUpdateUI.post(updateUIeachSecond);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
pauseTrackDataHub();
|
|
||||||
|
|
||||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||||
|
|
||||||
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -191,101 +160,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
sharedPreferences = null;
|
sharedPreferences = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackUpdated(final Track track) {
|
|
||||||
if (isResumed()) {
|
|
||||||
getActivity().runOnUiThread(() -> {
|
|
||||||
if (isResumed()) {
|
|
||||||
if ((this.track == null && track != null) || (this.track != null && track != null && !this.track.getCategory().equals(track.getCategory()))) {
|
|
||||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
|
|
||||||
}
|
|
||||||
this.track = track;
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearTrackPoints() {
|
|
||||||
lastTrackPoint = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint, @NonNull TrackStatistics trackStatistics) {
|
|
||||||
if (isResumed()) {
|
|
||||||
getActivity().runOnUiThread(() -> {
|
|
||||||
if (isResumed()) {
|
|
||||||
synchronized (this) {
|
|
||||||
this.lastTrackPoint = newLastTrackPoint;
|
|
||||||
|
|
||||||
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
|
|
||||||
this.lastTrackPoint = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastTrackPoint != null && this.lastTrackPoint.hasLocation() && !this.lastTrackPoint.isRecent()) {
|
|
||||||
this.lastTrackPoint = null;
|
|
||||||
}
|
|
||||||
setLocationValues();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearMarkers() {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewMarker(Marker marker) {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNewMarkersDone() {
|
|
||||||
// We don't care.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resumes the trackDataHub.
|
|
||||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
|
||||||
*/
|
|
||||||
private synchronized void resumeTrackDataHub() {
|
|
||||||
trackDataHub = ((TrackRecordingActivity) getActivity()).getTrackDataHub();
|
|
||||||
trackDataHub.registerTrackDataListener(this, true, false, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pauses the trackDataHub.
|
|
||||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
|
||||||
*/
|
|
||||||
private synchronized void pauseTrackDataHub() {
|
|
||||||
trackDataHub.unregisterTrackDataListener(this);
|
|
||||||
trackDataHub = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the selected track is recording.
|
|
||||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
|
||||||
*/
|
|
||||||
private synchronized boolean isSelectedTrackRecording() {
|
|
||||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the selected track is paused.
|
|
||||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
|
||||||
*/
|
|
||||||
private synchronized boolean isSelectedTrackPaused() {
|
|
||||||
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
private void updateUI() {
|
||||||
updateStats();
|
if (isResumed()) {
|
||||||
setLocationValues();
|
updateStats();
|
||||||
updateSensorDataUI();
|
setLocationValues();
|
||||||
|
updateSensorDataUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,100 +173,45 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
* Also sets altitude gain and loss.
|
* Also sets altitude gain and loss.
|
||||||
*/
|
*/
|
||||||
private void updateSensorDataUI() {
|
private void updateSensorDataUI() {
|
||||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||||
|
if (sensorDataSet != null) {
|
||||||
if (trackRecordingService == null) {
|
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||||
Log.d(TAG, "Cannot get the track recording service.");
|
if (sensorDataSet.getHeartRate() != null) {
|
||||||
} else {
|
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
||||||
SensorDataSet sensorDataSet = trackRecordingService.getSensorDataSet();
|
|
||||||
if (sensorDataSet != null) {
|
|
||||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
|
||||||
if (sensorDataSet.getHeartRate() != null) {
|
|
||||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
|
||||||
}
|
|
||||||
if (sensorDataSet.getCyclingCadence() != null) {
|
|
||||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
|
||||||
}
|
|
||||||
if (sensorDataSet.getCyclingPower() != null) {
|
|
||||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
|
||||||
}
|
|
||||||
sensorsAdapter.swapData(sensorDataList);
|
|
||||||
setSpeedSensorData(sensorDataSet);
|
|
||||||
}
|
}
|
||||||
//TODO Check if we can get the total altitude gain and loss via trackStatistics instead of doing some computation in the UI layer.
|
if (sensorDataSet.getCyclingCadence() != null) {
|
||||||
setTotalAltitudeGain(trackRecordingService.getAltitudeGain_m());
|
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
||||||
setTotalAltitudeLoss(trackRecordingService.getAltitudeLoss_m());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set altitude gain
|
|
||||||
private void setTotalAltitudeGain(Float altitudeGain_m) {
|
|
||||||
Float totalAltitudeGain = altitudeGain_m;
|
|
||||||
|
|
||||||
if (track != null && track.getTrackStatistics().hasTotalAltitudeGain()) {
|
|
||||||
if (altitudeGain_m == null) {
|
|
||||||
totalAltitudeGain = 0f;
|
|
||||||
}
|
}
|
||||||
|
if (sensorDataSet.getCyclingPower() != null) {
|
||||||
totalAltitudeGain += track.getTrackStatistics().getTotalAltitudeGain();
|
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
||||||
|
}
|
||||||
|
sensorsAdapter.swapData(sensorDataList);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeGain, preferenceMetricUnits);
|
{
|
||||||
viewBinding.statsAltitudeGainValue.setText(parts.first);
|
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
|
||||||
viewBinding.statsAltitudeGainUnit.setText(parts.second);
|
viewBinding.statsAltitudeGainValue.setText(parts.first);
|
||||||
}
|
viewBinding.statsAltitudeGainUnit.setText(parts.second);
|
||||||
|
|
||||||
// Set altitude loss
|
|
||||||
private void setTotalAltitudeLoss(Float altitudeLoss_m) {
|
|
||||||
Float totalAltitudeLoss = altitudeLoss_m;
|
|
||||||
|
|
||||||
if (track != null && track.getTrackStatistics().hasTotalAltitudeLoss()) {
|
|
||||||
if (altitudeLoss_m == null) {
|
|
||||||
totalAltitudeLoss = track.getTrackStatistics().getTotalAltitudeLoss();
|
|
||||||
} else {
|
|
||||||
totalAltitudeLoss += track.getTrackStatistics().getTotalAltitudeLoss();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
{
|
||||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeLoss, preferenceMetricUnits);
|
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
|
||||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||||
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
||||||
}
|
|
||||||
|
|
||||||
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
|
|
||||||
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
|
|
||||||
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
|
|
||||||
|
|
||||||
if (data.hasValue() && data.isRecent()) {
|
|
||||||
setTotalDistance(data.getValue().distanceOverall);
|
|
||||||
setSpeed(data.getValue().speed);
|
|
||||||
}
|
|
||||||
if (data.hasValue() && data.isRecent()) {
|
|
||||||
setSpeed(data.getValue().speed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStats() {
|
private void updateStats() {
|
||||||
|
|
||||||
setTotalDistance(Distance.of(0)); //TODO Why?
|
|
||||||
|
|
||||||
// Set activity type
|
// Set activity type
|
||||||
if (track != null) {
|
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
|
||||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
|
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
||||||
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
|
|
||||||
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set time
|
// Set time
|
||||||
if (track != null) {
|
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
|
||||||
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
|
updateTotalTime();
|
||||||
updateTotalTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set average speed/pace
|
// Set average speed/pace
|
||||||
{
|
{
|
||||||
Speed speed = track != null ? track.getTrackStatistics().getAverageSpeed() : null;
|
Speed speed = recordingData.getTrackStatistics().getAverageSpeed();
|
||||||
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||||
@@ -396,7 +221,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
|
|
||||||
// Set max speed/pace
|
// Set max speed/pace
|
||||||
{
|
{
|
||||||
Speed speed = track != null ? track.getTrackStatistics().getMaxSpeed() : null;
|
Speed speed = recordingData.getTrackStatistics().getMaxSpeed();
|
||||||
|
|
||||||
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||||
|
|
||||||
@@ -407,7 +232,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
|
|
||||||
// Set moving speed/pace
|
// Set moving speed/pace
|
||||||
{
|
{
|
||||||
Speed speed = track != null ? track.getTrackStatistics().getAverageMovingSpeed() : null;
|
Speed speed = recordingData.getTrackStatistics().getAverageMovingSpeed();
|
||||||
|
|
||||||
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||||
|
|
||||||
@@ -417,34 +242,41 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set altitude gain and loss
|
// Set altitude gain and loss
|
||||||
{
|
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTotalTime() {
|
private void updateTotalTime() {
|
||||||
Duration totalTime = track.getTrackStatistics().getTotalTime();
|
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
|
||||||
if (isSelectedTrackRecording()) {
|
|
||||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
|
||||||
if (trackRecordingService != null) {
|
|
||||||
totalTime = trackRecordingService.getTotalTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLocationValues() {
|
private void setLocationValues() {
|
||||||
// Set speed/pace
|
// Set speed/pace
|
||||||
Speed speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : null;
|
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
|
||||||
|
|
||||||
|
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||||
|
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
|
||||||
|
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
|
||||||
|
if (data.hasValue() && data.isRecent()) {
|
||||||
|
speed = data.getValue().speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSpeed(speed);
|
setSpeed(speed);
|
||||||
|
|
||||||
|
// Set distance
|
||||||
|
{
|
||||||
|
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), recordingData.getTrackStatistics().getTotalDistance(), preferenceMetricUnits);
|
||||||
|
|
||||||
|
viewBinding.statsDistanceValue.setText(parts.first);
|
||||||
|
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||||
|
}
|
||||||
// Set altitude
|
// Set altitude
|
||||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (preferenceShowAltitude) {
|
if (preferenceShowAltitude) {
|
||||||
// Current altitude
|
// Current altitude
|
||||||
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude().toM() : null;
|
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
|
||||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
|
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
|
||||||
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
|
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
|
||||||
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
|
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
|
||||||
@@ -455,23 +287,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
if (preferenceShowCoordinate) {
|
if (preferenceShowCoordinate) {
|
||||||
String latitudeText = getContext().getString(R.string.value_unknown);
|
String latitudeText = getContext().getString(R.string.value_unknown);
|
||||||
String longitudeText = getContext().getString(R.string.value_unknown);
|
String longitudeText = getContext().getString(R.string.value_unknown);
|
||||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
|
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
|
||||||
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
|
latitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLatitude());
|
||||||
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
|
longitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLongitude());
|
||||||
}
|
}
|
||||||
viewBinding.statsLatitudeValue.setText(latitudeText);
|
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||||
viewBinding.statsLongitudeValue.setText(longitudeText);
|
viewBinding.statsLongitudeValue.setText(longitudeText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTotalDistance(Distance sensorDistanceSinceLastTrackpoint) {
|
|
||||||
Distance totalDistance = track != null ? (track.getTrackStatistics().getTotalDistance().plus(sensorDistanceSinceLastTrackpoint)) : Distance.invalid();
|
|
||||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
|
|
||||||
|
|
||||||
viewBinding.statsDistanceValue.setText(parts.first);
|
|
||||||
viewBinding.statsDistanceUnit.setText(parts.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSpeed(Speed speed) {
|
private void setSpeed(Speed speed) {
|
||||||
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||||
|
|
||||||
@@ -479,4 +303,21 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
viewBinding.statsSpeedValue.setText(parts.first);
|
viewBinding.statsSpeedValue.setText(parts.first);
|
||||||
viewBinding.statsSpeedUnit.setText(parts.second);
|
viewBinding.statsSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onRecordingDataChanged(TrackRecordingService.RecordingData recordingData) {
|
||||||
|
String oldCategory = this.recordingData.getTrackCategory();
|
||||||
|
String newCategory = recordingData.getTrackCategory();
|
||||||
|
this.recordingData = recordingData;
|
||||||
|
|
||||||
|
if (!oldCategory.equals(newCategory)) {
|
||||||
|
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.latestTrackPoint = recordingData.getLatestTrackPoint();
|
||||||
|
if (latestTrackPoint != null && latestTrackPoint.hasLocation() && !latestTrackPoint.isRecent()) {
|
||||||
|
latestTrackPoint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
|||||||
*/
|
*/
|
||||||
public enum TrackFileFormat {
|
public enum TrackFileFormat {
|
||||||
|
|
||||||
|
@Deprecated //Not needed anymore; we now have OSMDashboard; was only used to share with other applications
|
||||||
KML_ONLY_TRACK {
|
KML_ONLY_TRACK {
|
||||||
@Override
|
@Override
|
||||||
public TrackExporter createTrackExporter(Context context) {
|
public TrackExporter createTrackExporter(Context context) {
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ public class GPXTrackExporter implements TrackExporter {
|
|||||||
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
|
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
|
||||||
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
|
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
|
||||||
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
||||||
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
|
|
||||||
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
|
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
|
||||||
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
|
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
|
||||||
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
|
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
|
||||||
|
|||||||
@@ -518,13 +518,12 @@ public class KMLTrackExporter implements TrackExporter {
|
|||||||
return location.getBearing();
|
return location.getBearing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCoordinates(Location location, String separator) {
|
private static String getCoordinates(Location location, String separator) {
|
||||||
StringBuilder builder = new StringBuilder();
|
String result = location.getLongitude() + separator + location.getLatitude();
|
||||||
builder.append(location.getLongitude()).append(separator).append(location.getLatitude());
|
|
||||||
if (location.hasAltitude()) {
|
if (location.hasAltitude()) {
|
||||||
builder.append(separator).append(location.getAltitude());
|
result += separator + location.getAltitude();
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,509 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.dennisguse.opentracks.io.file.importer;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.xml.sax.Locator;
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
import de.dennisguse.opentracks.content.data.Altitude;
|
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|
||||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
|
||||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
|
||||||
import de.dennisguse.opentracks.util.LocationUtils;
|
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class for file track importers.
|
|
||||||
*
|
|
||||||
* @author Jimmy Shih
|
|
||||||
*/
|
|
||||||
abstract class AbstractFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
|
||||||
|
|
||||||
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final ContentProviderUtils contentProviderUtils;
|
|
||||||
private final Distance recordingDistanceInterval;
|
|
||||||
|
|
||||||
private final List<Track.Id> trackIds = new ArrayList<>();
|
|
||||||
private final List<Marker> markers = new ArrayList<>();
|
|
||||||
|
|
||||||
// The current element content
|
|
||||||
//TODO Should be made private and getter be used by child classes.
|
|
||||||
protected String content;
|
|
||||||
|
|
||||||
protected String icon;
|
|
||||||
protected String name;
|
|
||||||
protected String description;
|
|
||||||
protected String category;
|
|
||||||
protected String latitude;
|
|
||||||
protected String longitude;
|
|
||||||
protected String altitude;
|
|
||||||
protected String time;
|
|
||||||
protected String speed;
|
|
||||||
protected String heartrate;
|
|
||||||
protected String cadence;
|
|
||||||
protected String power;
|
|
||||||
protected String markerType;
|
|
||||||
protected String photoUrl;
|
|
||||||
protected String uuid;
|
|
||||||
protected String gain;
|
|
||||||
protected String loss;
|
|
||||||
protected String distance;
|
|
||||||
|
|
||||||
// The current track data
|
|
||||||
private TrackData trackData;
|
|
||||||
|
|
||||||
// The SAX locator to get the current line information
|
|
||||||
private Locator locator;
|
|
||||||
|
|
||||||
private final SharedPreferences sharedPreferences;
|
|
||||||
|
|
||||||
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
|
||||||
this.context = context;
|
|
||||||
this.contentProviderUtils = contentProviderUtils;
|
|
||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
|
||||||
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setDocumentLocator(Locator locator) {
|
|
||||||
this.locator = locator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void characters(char[] ch, int start, int length) {
|
|
||||||
String newContent = new String(ch, start, length);
|
|
||||||
if (content == null) {
|
|
||||||
content = newContent;
|
|
||||||
} else {
|
|
||||||
// In 99% of the cases, a single call to this method will be made for each sequence of characters we're interested in, so we'll rarely be concatenating strings, thus not justifying the use of a StringBuilder.
|
|
||||||
content += newContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onFileEnd() {
|
|
||||||
// Add markers to the last imported track
|
|
||||||
int size = trackIds.size();
|
|
||||||
if (size == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Track.Id trackId = trackIds.get(size - 1);
|
|
||||||
Track track = contentProviderUtils.getTrack(trackId);
|
|
||||||
if (track == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int markerPosition = -1;
|
|
||||||
Marker marker = null;
|
|
||||||
TrackPoint trackPoint = null;
|
|
||||||
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
|
|
||||||
|
|
||||||
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
|
|
||||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (marker == null) {
|
|
||||||
markerPosition++;
|
|
||||||
marker = markerPosition < markers.size() ? markers.get(markerPosition) : null;
|
|
||||||
if (marker == null) {
|
|
||||||
// No more markers
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If marker had photo it must be translated to internal photo url (depend on track id)
|
|
||||||
if (marker.hasPhoto()) {
|
|
||||||
marker.setPhotoUrl(getInternalPhotoUrl(marker.getPhotoUrl()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackPoint == null) {
|
|
||||||
if (!trackPointIterator.hasNext()) {
|
|
||||||
// No more track points. Ignore the rest of the markers.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trackPoint = trackPointIterator.next();
|
|
||||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((marker.getTime()).isAfter(trackPoint.getTime())) {
|
|
||||||
trackPoint = null;
|
|
||||||
} else if (marker.getTime().isBefore(trackPoint.getTime())) {
|
|
||||||
Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
|
|
||||||
marker = null;
|
|
||||||
} else {
|
|
||||||
// The marker trackPoint time matches the track point time
|
|
||||||
if (!trackPoint.hasLocation()) {
|
|
||||||
// Invalid trackPoint, load the next trackPoint
|
|
||||||
trackPoint = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid trackPoint
|
|
||||||
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
|
|
||||||
String markerDescription = marker.getDescription();
|
|
||||||
String icon = context.getString(R.string.marker_icon_url);
|
|
||||||
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
|
|
||||||
|
|
||||||
// Insert marker
|
|
||||||
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
|
|
||||||
contentProviderUtils.insertMarker(newMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the next marker
|
|
||||||
marker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On track start.
|
|
||||||
*/
|
|
||||||
protected void onTrackStart() throws SAXException {
|
|
||||||
trackData = new TrackData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On track end.
|
|
||||||
*/
|
|
||||||
protected void onTrackEnd() {
|
|
||||||
if (name != null) {
|
|
||||||
trackData.track.setName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
trackData.track.setUuid(UUID.fromString(uuid));
|
|
||||||
} catch (IllegalArgumentException | NullPointerException e) {
|
|
||||||
Log.w(TAG, "could not parse Track UUID, generating a new one.");
|
|
||||||
trackData.track.setUuid(UUID.randomUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description != null) {
|
|
||||||
trackData.track.setDescription(description);
|
|
||||||
}
|
|
||||||
if (category != null) {
|
|
||||||
trackData.track.setCategory(category);
|
|
||||||
//TODO remove when GPX and KML support reading this property.
|
|
||||||
if (icon == null)
|
|
||||||
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
|
|
||||||
}
|
|
||||||
if (icon != null) {
|
|
||||||
trackData.track.setIcon(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackStatisticsUpdater statistics = new TrackStatisticsUpdater();
|
|
||||||
//TODO I guess, we should not filter by recordingDistanceInterval on import; the data is already recorded, so we should not change it.
|
|
||||||
for (TrackPoint trackPoint : trackData.bufferedTrackPoints) {
|
|
||||||
statistics.addTrackPoint(trackPoint, recordingDistanceInterval);
|
|
||||||
}
|
|
||||||
if (!statistics.isTrackInitialized()) {
|
|
||||||
throw new ImportParserException("Track did not contain any locations.");
|
|
||||||
}
|
|
||||||
trackData.track.setTrackStatistics(statistics.getTrackStatistics());
|
|
||||||
|
|
||||||
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
|
|
||||||
if (track != null) {
|
|
||||||
if (PreferencesUtils.getPreventReimportTracks(sharedPreferences, context)) {
|
|
||||||
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO This is a workaround until we have proper UI.
|
|
||||||
trackData.track.setUuid(UUID.randomUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackIds.size() > 0) {
|
|
||||||
// TODO Multi track is not supported yet.
|
|
||||||
cleanImport();
|
|
||||||
throw new ImportParserException("Multi track not supported");
|
|
||||||
}
|
|
||||||
Uri uri = contentProviderUtils.insertTrack(trackData.track);
|
|
||||||
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
|
|
||||||
trackIds.add(trackId);
|
|
||||||
trackData.track.setId(trackId);
|
|
||||||
|
|
||||||
flushTrackPoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onTrackSegmentStart() {
|
|
||||||
trackData.numberOfSegments++;
|
|
||||||
|
|
||||||
//If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
|
|
||||||
if (trackData.numberOfSegments > 1
|
|
||||||
&& trackData.lastLocationInCurrentSegment != null
|
|
||||||
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL))
|
|
||||||
) {
|
|
||||||
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
|
|
||||||
}
|
|
||||||
trackData.lastLocationInCurrentSegment = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onTrackSegmentEnd() {
|
|
||||||
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
|
|
||||||
if (trackPoint == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trackPoint.hasLocation()) {
|
|
||||||
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void addMarker() throws ParsingException {
|
|
||||||
// Markers must have a time, else cannot match to the track points
|
|
||||||
if (time == null) {
|
|
||||||
Log.w(TAG, "Marker without time ignored.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackPoint trackPoint = createTrackPoint();
|
|
||||||
|
|
||||||
if (!trackPoint.hasLocation()) {
|
|
||||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Marker marker = new Marker(null, trackPoint);
|
|
||||||
|
|
||||||
if (name != null) {
|
|
||||||
marker.setName(name);
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
marker.setDescription(description);
|
|
||||||
}
|
|
||||||
if (category != null) {
|
|
||||||
marker.setCategory(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (photoUrl != null) {
|
|
||||||
marker.setPhotoUrl(photoUrl);
|
|
||||||
}
|
|
||||||
markers.add(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TrackPoint getTrackPoint() throws ParsingException {
|
|
||||||
TrackPoint trackPoint = createTrackPoint();
|
|
||||||
|
|
||||||
if (trackPoint.hasLocation()) {
|
|
||||||
Instant time = trackPoint.getTime();
|
|
||||||
if (trackPoint.getLatitude() == 100) {
|
|
||||||
//TODO Remove by 31st December 2021.
|
|
||||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
|
||||||
trackPoint.setTime(time);
|
|
||||||
} else if (trackPoint.getLatitude() == 200) {
|
|
||||||
//TODO Remove by 31st December 2021.
|
|
||||||
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL);
|
|
||||||
trackPoint.setTime(time);
|
|
||||||
|
|
||||||
} else if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
|
||||||
throw new ParsingException(createErrorMessage("Invalid location detected: " + trackPoint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate derived attributes from the previous point
|
|
||||||
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() == null) {
|
|
||||||
if (!trackPoint.hasSpeed()) {
|
|
||||||
Duration timeDifference = Duration.between(trackData.lastLocationInCurrentSegment.getTime(), trackPoint.getTime());
|
|
||||||
|
|
||||||
// Check for negative time change
|
|
||||||
if (timeDifference.isNegative()) {
|
|
||||||
Log.w(TAG, "Time difference not positive.");
|
|
||||||
} else {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We don't have a speed and bearing in GPX, make something up from the last two points.
|
|
||||||
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
|
|
||||||
*/
|
|
||||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
|
||||||
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
|
|
||||||
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
|
|
||||||
|
|
||||||
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
|
|
||||||
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
|
|
||||||
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
|
|
||||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trackData.lastLocationInCurrentSegment = trackPoint;
|
|
||||||
return trackPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an error message.
|
|
||||||
*
|
|
||||||
* @param message the message
|
|
||||||
*/
|
|
||||||
protected String createErrorMessage(String message) {
|
|
||||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the photo url for a file.
|
|
||||||
*
|
|
||||||
* @param externalPhotoUrl the file name
|
|
||||||
*/
|
|
||||||
protected String getInternalPhotoUrl(String externalPhotoUrl) {
|
|
||||||
if (trackData.track.getId() == null) {
|
|
||||||
Log.e(TAG, "Track id is invalid.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (externalPhotoUrl == null) {
|
|
||||||
Log.i(TAG, "External photo url is null.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
|
|
||||||
File file = FileUtils.buildInternalPhotoFile(context, trackData.track.getId(), Uri.parse(importFileName));
|
|
||||||
if (file != null) {
|
|
||||||
Uri photoUri = FileUtils.getUriForFile(context, file);
|
|
||||||
return "" + photoUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected TrackPoint createTrackPoint() throws ParsingException {
|
|
||||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
|
||||||
|
|
||||||
try {
|
|
||||||
trackPoint.setTime(StringUtils.parseTime(time));
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latitude == null || longitude == null) {
|
|
||||||
return trackPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
trackPoint.setLatitude(Double.parseDouble(latitude));
|
|
||||||
trackPoint.setLongitude(Double.parseDouble(longitude));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (altitude != null) {
|
|
||||||
try {
|
|
||||||
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a trackPoint.
|
|
||||||
*
|
|
||||||
* @param trackPoint the trackPoint
|
|
||||||
*/
|
|
||||||
protected void insertTrackPoint(TrackPoint trackPoint) {
|
|
||||||
trackData.bufferedTrackPoints.add(trackPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isFirstTrackPointInSegment() {
|
|
||||||
return trackData.lastLocationInCurrentSegment == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes the TrackPoints to the database.
|
|
||||||
*/
|
|
||||||
private void flushTrackPoints() {
|
|
||||||
if (trackData.bufferedTrackPoints.size() > 0) {
|
|
||||||
contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
|
|
||||||
trackData.bufferedTrackPoints.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Track.Id> getImportTrackIds() {
|
|
||||||
return trackIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanImport() {
|
|
||||||
contentProviderUtils.deleteTracks(context, trackIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data for the current track.
|
|
||||||
*
|
|
||||||
* @author Jimmy Shih
|
|
||||||
*/
|
|
||||||
// TODO Why private inner class?
|
|
||||||
private static class TrackData {
|
|
||||||
// The current track
|
|
||||||
final Track track = new Track();
|
|
||||||
|
|
||||||
// The number of segments processed for the current track
|
|
||||||
int numberOfSegments = 0;
|
|
||||||
|
|
||||||
// The last location in the current segment; Null if the current segment doesn't have a last location
|
|
||||||
TrackPoint lastLocationInCurrentSegment;
|
|
||||||
|
|
||||||
// The buffered locations
|
|
||||||
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ParsingException extends RuntimeException {
|
|
||||||
|
|
||||||
private ParsingException(@NonNull String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ParsingException(@NonNull String message, Exception cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "" + getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,19 +17,25 @@
|
|||||||
package de.dennisguse.opentracks.io.file.importer;
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
|
import org.xml.sax.Locator;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.content.data.Altitude;
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports a GPX file.
|
* Imports a GPX file.
|
||||||
@@ -43,7 +49,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|||||||
*
|
*
|
||||||
* @author Jimmy Shih
|
* @author Jimmy Shih
|
||||||
*/
|
*/
|
||||||
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
public class GpxTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
||||||
|
|
||||||
|
private static final String TAG = GpxTrackImporter.class.getSimpleName();
|
||||||
|
|
||||||
private static final String TAG_DESCRIPTION = "desc";
|
private static final String TAG_DESCRIPTION = "desc";
|
||||||
private static final String TAG_COMMENT = "cmt";
|
private static final String TAG_COMMENT = "cmt";
|
||||||
@@ -70,23 +78,46 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
|
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
|
||||||
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
|
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
|
||||||
|
|
||||||
/**
|
private Locator locator;
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
*/
|
|
||||||
public GpxFileTrackImporter(Context context) {
|
|
||||||
this(context, new ContentProviderUtils(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
private final Context context;
|
||||||
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
|
||||||
super(context, contentProviderUtils);
|
// Belongs to the current track
|
||||||
|
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||||
|
|
||||||
|
// The current element content
|
||||||
|
private String content = "";
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String category;
|
||||||
|
private String latitude;
|
||||||
|
private String longitude;
|
||||||
|
private String altitude;
|
||||||
|
private String time;
|
||||||
|
private String speed;
|
||||||
|
private String heartrate;
|
||||||
|
private String cadence;
|
||||||
|
private String power;
|
||||||
|
protected String markerType;
|
||||||
|
protected String photoUrl;
|
||||||
|
protected String uuid;
|
||||||
|
protected String gain;
|
||||||
|
protected String loss;
|
||||||
|
protected String distance;
|
||||||
|
|
||||||
|
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
|
||||||
|
|
||||||
|
private final TrackImporter trackImporter;
|
||||||
|
|
||||||
|
public GpxTrackImporter(Context context, TrackImporter trackImporter) {
|
||||||
|
this.context = context;
|
||||||
|
this.trackImporter = trackImporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DefaultHandler getHandler() {
|
public void setDocumentLocator(Locator locator) {
|
||||||
return this;
|
this.locator = locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -96,7 +127,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
onMarkerStart(attributes);
|
onMarkerStart(attributes);
|
||||||
break;
|
break;
|
||||||
case TAG_TRACK:
|
case TAG_TRACK:
|
||||||
onTrackStart();
|
trackImporter.newTrack();
|
||||||
break;
|
break;
|
||||||
case TAG_TRACK_SEGMENT:
|
case TAG_TRACK_SEGMENT:
|
||||||
onTrackSegmentStart();
|
onTrackSegmentStart();
|
||||||
@@ -107,6 +138,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void characters(char[] ch, int start, int length) {
|
||||||
|
content += new String(ch, start, length);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void endElement(String uri, String localName, String tag) {
|
public void endElement(String uri, String localName, String tag) {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
@@ -117,13 +153,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
onMarkerEnd();
|
onMarkerEnd();
|
||||||
break;
|
break;
|
||||||
case TAG_TRACK:
|
case TAG_TRACK:
|
||||||
onTrackEnd();
|
trackImporter.setTrack(context, name, uuid, description, category, null);
|
||||||
break;
|
break;
|
||||||
case TAG_TRACK_SEGMENT:
|
case TAG_TRACK_SEGMENT:
|
||||||
onTrackSegmentEnd();
|
onTrackSegmentEnd();
|
||||||
break;
|
break;
|
||||||
case TAG_TRACK_POINT:
|
case TAG_TRACK_POINT:
|
||||||
onTrackPointEnd();
|
currentSegment.add(createTrackPoint());
|
||||||
break;
|
break;
|
||||||
case TAG_NAME:
|
case TAG_NAME:
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
@@ -197,33 +233,65 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset element content
|
content = "";
|
||||||
content = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void onTrackSegmentStart() {
|
||||||
protected void onTrackStart() throws SAXException {
|
|
||||||
super.onTrackStart();
|
|
||||||
name = null;
|
|
||||||
description = null;
|
|
||||||
category = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void onTrackSegmentEnd() {
|
||||||
protected TrackPoint createTrackPoint() throws ParsingException {
|
if (currentSegment.isEmpty()) {
|
||||||
TrackPoint trackPoint = super.createTrackPoint();
|
Log.w(TAG, "No locations in current segment.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackPoint first = currentSegment.getFirst();
|
||||||
|
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||||
|
|
||||||
|
trackImporter.addTrackPoints(currentSegment);
|
||||||
|
currentSegment.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private TrackPoint createTrackPoint() throws ParsingException {
|
||||||
|
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
trackPoint.setTime(StringUtils.parseTime(time));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latitude == null || longitude == null) {
|
||||||
|
return trackPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
trackPoint.setLatitude(Double.parseDouble(latitude));
|
||||||
|
trackPoint.setLongitude(Double.parseDouble(longitude));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (altitude != null) {
|
||||||
|
try {
|
||||||
|
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (speed != null) {
|
if (speed != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setSpeed(Speed.of(speed));
|
trackPoint.setSpeed(Speed.of(speed));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (heartrate != null) {
|
if (heartrate != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
|
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +307,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
if (power != null) {
|
if (power != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setPower(Float.parseFloat(power));
|
trackPoint.setPower(Float.parseFloat(power));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,21 +315,21 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
if (gain != null) {
|
if (gain != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setAltitudeGain(Float.parseFloat(gain));
|
trackPoint.setAltitudeGain(Float.parseFloat(gain));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s", gain)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loss != null) {
|
if (loss != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
|
trackPoint.setAltitudeLoss(Float.parseFloat(loss));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude loss: %s", loss)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (distance != null) {
|
if (distance != null) {
|
||||||
try {
|
try {
|
||||||
trackPoint.setSensorDistance(Distance.of(distance));
|
trackPoint.setSensorDistance(Distance.of(distance));
|
||||||
} catch (Exception e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,11 +337,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
return trackPoint;
|
return trackPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On track point start.
|
|
||||||
*
|
|
||||||
* @param attributes the attributes
|
|
||||||
*/
|
|
||||||
private void onTrackPointStart(Attributes attributes) {
|
private void onTrackPointStart(Attributes attributes) {
|
||||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||||
@@ -285,15 +348,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
loss = null;
|
loss = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTrackPointEnd() {
|
|
||||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
|
||||||
TrackPoint trackPoint = getTrackPoint();
|
|
||||||
if (isFirstTrackPointInSegment) {
|
|
||||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
|
||||||
}
|
|
||||||
insertTrackPoint(trackPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMarkerStart(Attributes attributes) {
|
private void onMarkerStart(Attributes attributes) {
|
||||||
name = null;
|
name = null;
|
||||||
description = null;
|
description = null;
|
||||||
@@ -307,6 +361,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onMarkerEnd() {
|
private void onMarkerEnd() {
|
||||||
addMarker();
|
// Markers must have a time, else cannot match to the track points
|
||||||
|
if (time == null) {
|
||||||
|
Log.w(TAG, "Marker without time; ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackPoint trackPoint = createTrackPoint();
|
||||||
|
|
||||||
|
if (!trackPoint.hasLocation()) {
|
||||||
|
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Marker marker = new Marker(null, trackPoint);
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
marker.setName(name);
|
||||||
|
}
|
||||||
|
if (description != null) {
|
||||||
|
marker.setDescription(description);
|
||||||
|
}
|
||||||
|
if (category != null) {
|
||||||
|
marker.setCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photoUrl != null) {
|
||||||
|
marker.setPhotoUrl(photoUrl);
|
||||||
|
}
|
||||||
|
markers.add(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createErrorMessage(String message) {
|
||||||
|
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFileEnd() {
|
||||||
|
trackImporter.addMarkers(markers);
|
||||||
|
trackImporter.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultHandler getHandler() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Track.Id> getImportTrackIds() {
|
||||||
|
return trackImporter.getTrackIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanImport() {
|
||||||
|
trackImporter.cleanImport();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.io.file.importer;
|
|||||||
import android.app.job.JobService;
|
import android.app.job.JobService;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.ResultReceiver;
|
import android.os.ResultReceiver;
|
||||||
@@ -16,9 +17,12 @@ import java.io.IOException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
|
|
||||||
public class ImportService extends JobIntentService {
|
public class ImportService extends JobIntentService {
|
||||||
|
|
||||||
@@ -50,13 +54,19 @@ public class ImportService extends JobIntentService {
|
|||||||
|
|
||||||
String fileExtension = FileUtils.getExtension(file);
|
String fileExtension = FileUtils.getExtension(file);
|
||||||
try {
|
try {
|
||||||
|
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||||
|
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, this);
|
||||||
|
Distance recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, this);
|
||||||
|
boolean preventReimport = PreferencesUtils.getPreventReimportTracks(sharedPreferences, this);
|
||||||
|
|
||||||
|
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), recordingDistanceInterval, maxRecordingDistance, preventReimport);
|
||||||
|
|
||||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||||
trackIds.addAll(new XMLImporter(new GpxFileTrackImporter(this)).importFile(this, file.getUri()));
|
trackIds.addAll(new XMLImporter(new GpxTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||||
trackIds.addAll(new XMLImporter(new KmlFileTrackImporter(this)).importFile(this, file.getUri()));
|
trackIds.addAll(new XMLImporter(new KmlTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||||
trackIds.addAll(new KmzTrackImporter().importFile(this, file.getUri()));
|
trackIds.addAll(new KmzTrackImporter(this, trackImporter).importFile(file.getUri()));
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Unsupported file format.");
|
Log.d(TAG, "Unsupported file format.");
|
||||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
|
||||||
|
|||||||
@@ -17,30 +17,35 @@
|
|||||||
package de.dennisguse.opentracks.io.file.importer;
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.location.Location;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import org.xml.sax.Attributes;
|
import org.xml.sax.Attributes;
|
||||||
|
import org.xml.sax.Locator;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|
||||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||||
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports a KML file.
|
* Imports a KML file.
|
||||||
*
|
*
|
||||||
* @author Jimmy Shih
|
* @author Jimmy Shih
|
||||||
*/
|
*/
|
||||||
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
public class KmlTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
|
||||||
|
|
||||||
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
|
private static final String TAG = KmlTrackImporter.class.getSimpleName();
|
||||||
|
|
||||||
private static final String MARKER_STYLE = "#" + KMLTrackExporter.MARKER_STYLE;
|
private static final String MARKER_STYLE = "#" + KMLTrackExporter.MARKER_STYLE;
|
||||||
|
|
||||||
@@ -64,29 +69,49 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
|
|
||||||
private static final String ATTRIBUTE_NAME = "name";
|
private static final String ATTRIBUTE_NAME = "name";
|
||||||
|
|
||||||
private boolean trackStarted = false;
|
private Locator locator;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
// Belongs to the current track
|
||||||
|
private final ArrayList<Instant> whenList = new ArrayList<>();
|
||||||
|
private final ArrayList<Location> locationList = new ArrayList<>();
|
||||||
|
|
||||||
private String extendedDataType;
|
private String extendedDataType;
|
||||||
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
|
||||||
private final ArrayList<Float> speedList = new ArrayList<>();
|
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
|
||||||
private final ArrayList<Float> distanceList = new ArrayList<>();
|
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
|
||||||
private final ArrayList<Float> cadenceList = new ArrayList<>();
|
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
|
||||||
private final ArrayList<Float> heartRateList = new ArrayList<>();
|
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
|
||||||
private final ArrayList<Float> powerList = new ArrayList<>();
|
|
||||||
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
|
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
|
||||||
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
|
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
|
||||||
|
|
||||||
public KmlFileTrackImporter(Context context) {
|
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||||
this(context, new ContentProviderUtils(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
// The current element content
|
||||||
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
private String content = "";
|
||||||
super(context, contentProviderUtils);
|
|
||||||
|
private String icon;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String category;
|
||||||
|
private String latitude;
|
||||||
|
private String longitude;
|
||||||
|
private String altitude;
|
||||||
|
private String markerType;
|
||||||
|
private String photoUrl;
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
private final TrackImporter trackImporter;
|
||||||
|
|
||||||
|
public KmlTrackImporter(Context context, TrackImporter trackImporter) {
|
||||||
|
this.context = context;
|
||||||
|
this.trackImporter = trackImporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DefaultHandler getHandler() {
|
public void setDocumentLocator(Locator locator) {
|
||||||
return this;
|
this.locator = locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,11 +123,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
onMarkerStart();
|
onMarkerStart();
|
||||||
break;
|
break;
|
||||||
case TAG_GX_MULTI_TRACK:
|
case TAG_GX_MULTI_TRACK:
|
||||||
trackStarted = true;
|
trackImporter.newTrack();
|
||||||
onTrackStart();
|
|
||||||
break;
|
break;
|
||||||
case TAG_GX_TRACK:
|
case TAG_GX_TRACK:
|
||||||
if (!trackStarted) {
|
if (trackImporter == null) {
|
||||||
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
||||||
}
|
}
|
||||||
onTrackSegmentStart();
|
onTrackSegmentStart();
|
||||||
@@ -113,6 +137,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void characters(char[] ch, int start, int length) {
|
||||||
|
content += new String(ch, start, length);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void endElement(String uri, String localName, String tag) throws SAXException {
|
public void endElement(String uri, String localName, String tag) throws SAXException {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
@@ -128,13 +157,13 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
onMarkerLocationEnd();
|
onMarkerLocationEnd();
|
||||||
break;
|
break;
|
||||||
case TAG_GX_MULTI_TRACK:
|
case TAG_GX_MULTI_TRACK:
|
||||||
onTrackEnd();
|
trackImporter.setTrack(context, name, uuid, description, category, icon);
|
||||||
break;
|
break;
|
||||||
case TAG_GX_TRACK:
|
case TAG_GX_TRACK:
|
||||||
onTrackSegmentEnd();
|
onTrackSegmentEnd();
|
||||||
break;
|
break;
|
||||||
case TAG_GX_COORD:
|
case TAG_GX_COORD:
|
||||||
onTrackPointEnd();
|
onCoordEnded();
|
||||||
break;
|
break;
|
||||||
case TAG_GX_VALUE:
|
case TAG_GX_VALUE:
|
||||||
onExtendedDataValueEnd();
|
onExtendedDataValueEnd();
|
||||||
@@ -166,7 +195,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
break;
|
break;
|
||||||
case TAG_WHEN:
|
case TAG_WHEN:
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
time = content.trim();
|
whenList.add(StringUtils.parseTime(content.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -183,7 +212,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset element content
|
// Reset element content
|
||||||
content = null;
|
content = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMarkerStart() {
|
private void onMarkerStart() {
|
||||||
@@ -196,7 +225,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
latitude = null;
|
latitude = null;
|
||||||
longitude = null;
|
longitude = null;
|
||||||
altitude = null;
|
altitude = null;
|
||||||
time = null;
|
|
||||||
markerType = null;
|
markerType = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +232,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
if (!MARKER_STYLE.equals(markerType)) {
|
if (!MARKER_STYLE.equals(markerType)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addMarker();
|
|
||||||
|
if (whenList.size() != 1) {
|
||||||
|
Log.w(TAG, "Marker without time ignored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location location = createLocation(longitude, latitude, altitude);
|
||||||
|
if (location == null) {
|
||||||
|
Log.w(TAG, "Marker with invalid coordinates ignored: " + location);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Marker marker = new Marker(null, new TrackPoint(TrackPoint.Type.TRACKPOINT, location, whenList.get(0))); //TODO Creating marker without need
|
||||||
|
marker.setName(name != null ? name : "");
|
||||||
|
marker.setDescription(description != null ? description : "");
|
||||||
|
marker.setCategory(category != null ? category : "");
|
||||||
|
marker.setPhotoUrl(photoUrl);
|
||||||
|
markers.add(marker);
|
||||||
|
|
||||||
|
name = null;
|
||||||
|
description = null;
|
||||||
|
category = null;
|
||||||
|
photoUrl = null;
|
||||||
|
whenList.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMarkerLocationEnd() {
|
private void onMarkerLocationEnd() {
|
||||||
@@ -219,39 +270,56 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void onTrackSegmentStart() {
|
||||||
protected void onTrackSegmentStart() {
|
locationList.clear();
|
||||||
super.onTrackSegmentStart();
|
whenList.clear();
|
||||||
trackPoints.clear();
|
|
||||||
speedList.clear();
|
sensorSpeedList.clear();
|
||||||
distanceList.clear();
|
sensorDistanceList.clear();
|
||||||
heartRateList.clear();
|
sensorHeartRateList.clear();
|
||||||
cadenceList.clear();
|
sensorCadenceList.clear();
|
||||||
powerList.clear();
|
sensorPowerList.clear();
|
||||||
altitudeGainList.clear();
|
altitudeGainList.clear();
|
||||||
altitudeLossList.clear();
|
altitudeLossList.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onTrackSegmentEnd() {
|
private void onTrackSegmentEnd() {
|
||||||
super.onTrackSegmentEnd();
|
if (locationList.size() != whenList.size()) {
|
||||||
// Close a track segment by inserting the segment locations
|
throw new ImportParserException("<coords> and <when> should have the same count.");
|
||||||
for (int i = 0; i < trackPoints.size(); i++) {
|
}
|
||||||
TrackPoint trackPoint = trackPoints.get(i);
|
|
||||||
|
|
||||||
if (i < speedList.size() && speedList.get(i) != null) {
|
// Close a track segment by inserting the segment locations
|
||||||
trackPoint.setSpeed(Speed.of(speedList.get(i)));
|
for (int i = 0; i < locationList.size(); i++) {
|
||||||
|
Instant time = whenList.get(i);
|
||||||
|
Location location = locationList.get(i);
|
||||||
|
|
||||||
|
TrackPoint trackPoint;
|
||||||
|
if (i == 0) {
|
||||||
|
if (location == null) {
|
||||||
|
trackPoint = TrackPoint.createSegmentStartManualWithTime(time);
|
||||||
|
} else {
|
||||||
|
trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, location, time);
|
||||||
|
}
|
||||||
|
} else if (i == locationList.size() - 1 && location == null) {
|
||||||
|
trackPoint = TrackPoint.createSegmentEndWithTime(time);
|
||||||
|
} else {
|
||||||
|
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, location, time);
|
||||||
}
|
}
|
||||||
if (i < distanceList.size() && distanceList.get(i) != null) {
|
|
||||||
trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
|
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
|
||||||
|
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
|
||||||
}
|
}
|
||||||
if (i < heartRateList.size()) {
|
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
|
||||||
trackPoint.setHeartRate_bpm(heartRateList.get(i));
|
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
|
||||||
}
|
}
|
||||||
if (i < cadenceList.size()) {
|
if (i < sensorHeartRateList.size()) {
|
||||||
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
|
trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i));
|
||||||
}
|
}
|
||||||
if (i < powerList.size()) {
|
if (i < sensorCadenceList.size()) {
|
||||||
trackPoint.setPower(powerList.get(i));
|
trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i));
|
||||||
|
}
|
||||||
|
if (i < sensorPowerList.size()) {
|
||||||
|
trackPoint.setPower(sensorPowerList.get(i));
|
||||||
}
|
}
|
||||||
if (i < altitudeGainList.size()) {
|
if (i < altitudeGainList.size()) {
|
||||||
trackPoint.setAltitudeGain(altitudeGainList.get(i));
|
trackPoint.setAltitudeGain(altitudeGainList.get(i));
|
||||||
@@ -260,18 +328,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
|
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
insertTrackPoint(trackPoint);
|
trackImporter.addTrackPoint(trackPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void onCoordEnded() {
|
||||||
* On track point end. gx:coord end tag.
|
|
||||||
*/
|
|
||||||
private void onTrackPointEnd() {
|
|
||||||
// Add trackPoint to trackPoints
|
|
||||||
if (content == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String[] parts = content.trim().split(" ");
|
String[] parts = content.trim().split(" ");
|
||||||
if (parts.length == 2 || parts.length == 3) {
|
if (parts.length == 2 || parts.length == 3) {
|
||||||
longitude = parts[0];
|
longitude = parts[0];
|
||||||
@@ -279,33 +340,41 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
altitude = parts.length == 3 ? parts[2] : null;
|
altitude = parts.length == 3 ? parts[2] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to GPX
|
locationList.add(createLocation(longitude, latitude, altitude));
|
||||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
|
||||||
TrackPoint trackPoint = getTrackPoint();
|
|
||||||
if (isFirstTrackPointInSegment) {
|
|
||||||
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
|
|
||||||
|
|
||||||
trackPoint.setType(type);
|
|
||||||
}
|
|
||||||
trackPoints.add(trackPoint);
|
|
||||||
|
|
||||||
// Reset variables for next trackpoint (which might not have such data).
|
|
||||||
time = null;
|
|
||||||
longitude = null;
|
longitude = null;
|
||||||
latitude = null;
|
latitude = null;
|
||||||
altitude = null;
|
altitude = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private Location createLocation(String longitude, String latitude, String altitude) {
|
||||||
* On extended data start. gx:SimpleArrayData start tag.
|
Location location = null;
|
||||||
*/
|
if (longitude != null || latitude != null) {
|
||||||
|
location = new Location("import");
|
||||||
|
|
||||||
|
try {
|
||||||
|
location.setLatitude(Double.parseDouble(latitude));
|
||||||
|
location.setLongitude(Double.parseDouble(longitude));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (altitude != null) {
|
||||||
|
try {
|
||||||
|
location.setAltitude(Double.parseDouble(altitude));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void onExtendedDataStart(Attributes attributes) {
|
private void onExtendedDataStart(Attributes attributes) {
|
||||||
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
|
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* On extended data value end. gx:value end tag.
|
|
||||||
*/
|
|
||||||
private void onExtendedDataValueEnd() throws SAXException {
|
private void onExtendedDataValueEnd() throws SAXException {
|
||||||
Float value = null;
|
Float value = null;
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
@@ -320,19 +389,19 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
}
|
}
|
||||||
switch (extendedDataType) {
|
switch (extendedDataType) {
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
||||||
speedList.add(value);
|
sensorSpeedList.add(value);
|
||||||
break;
|
break;
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
|
||||||
distanceList.add(value);
|
sensorDistanceList.add(value);
|
||||||
break;
|
break;
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
|
||||||
powerList.add(value);
|
sensorPowerList.add(value);
|
||||||
break;
|
break;
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
|
||||||
heartRateList.add(value);
|
sensorHeartRateList.add(value);
|
||||||
break;
|
break;
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
|
||||||
cadenceList.add(value);
|
sensorCadenceList.add(value);
|
||||||
break;
|
break;
|
||||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
|
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
|
||||||
altitudeGainList.add(value);
|
altitudeGainList.add(value);
|
||||||
@@ -344,4 +413,28 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
|
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String createErrorMessage(String message) {
|
||||||
|
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFileEnd() {
|
||||||
|
trackImporter.addMarkers(markers);
|
||||||
|
trackImporter.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultHandler getHandler() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Track.Id> getImportTrackIds() {
|
||||||
|
return trackImporter.getTrackIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanImport() {
|
||||||
|
trackImporter.cleanImport();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -51,11 +51,16 @@ public class KmzTrackImporter {
|
|||||||
|
|
||||||
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
|
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
|
||||||
|
|
||||||
private Context context;
|
private final Context context;
|
||||||
|
private final TrackImporter trackImporter;
|
||||||
|
|
||||||
|
public KmzTrackImporter(Context context, TrackImporter trackImporter) {
|
||||||
|
this.context = context;
|
||||||
|
this.trackImporter = trackImporter;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
|
public List<Track.Id> importFile(Uri fileUri) throws IOException {
|
||||||
this.context = context;
|
|
||||||
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
|
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
|
||||||
|
|
||||||
List<Track.Id> trackIdsWithImages = new ArrayList<>();
|
List<Track.Id> trackIdsWithImages = new ArrayList<>();
|
||||||
@@ -205,7 +210,7 @@ public class KmzTrackImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
|
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
|
||||||
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||||
|
|
||||||
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
|
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ParsingException extends RuntimeException {
|
||||||
|
|
||||||
|
private ParsingException(@NonNull String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ParsingException(@NonNull String message, Exception cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "" + getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
|
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||||
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
import de.dennisguse.opentracks.util.LocationUtils;
|
||||||
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles logic to import:
|
||||||
|
* 1. addTrackPoints()
|
||||||
|
* 2. addMarkers();
|
||||||
|
* 3. setTrack();
|
||||||
|
* 4. newTrack(); //stores current track to databse
|
||||||
|
* 5. if needed go to 1.
|
||||||
|
* 6. finish()
|
||||||
|
* <p>
|
||||||
|
* NOTE: This class modifies the parameter.
|
||||||
|
* Do not re-use these objects anywhere else.
|
||||||
|
*/
|
||||||
|
public class TrackImporter {
|
||||||
|
|
||||||
|
private static final String TAG = TrackImporter.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final ContentProviderUtils contentProviderUtils;
|
||||||
|
|
||||||
|
private final Distance recordingDistanceInterval;
|
||||||
|
private final Distance maxRecordingDistance;
|
||||||
|
private final boolean preventReimport;
|
||||||
|
|
||||||
|
private final List<Track.Id> trackIds = new ArrayList<>();
|
||||||
|
|
||||||
|
// Current track
|
||||||
|
private Track track;
|
||||||
|
private final List<TrackPoint> trackPoints = new LinkedList<>();
|
||||||
|
private final List<Marker> markers = new LinkedList<>();
|
||||||
|
|
||||||
|
public TrackImporter(Context context, ContentProviderUtils contentProviderUtils, Distance recordingDistanceInterval, Distance maxRecordingDistance, boolean preventReimport) {
|
||||||
|
this.context = context;
|
||||||
|
this.contentProviderUtils = contentProviderUtils;
|
||||||
|
this.recordingDistanceInterval = recordingDistanceInterval;
|
||||||
|
this.maxRecordingDistance = maxRecordingDistance;
|
||||||
|
this.preventReimport = preventReimport;
|
||||||
|
}
|
||||||
|
|
||||||
|
void newTrack() {
|
||||||
|
if (track != null) {
|
||||||
|
finishTrack();
|
||||||
|
}
|
||||||
|
|
||||||
|
track = null;
|
||||||
|
trackPoints.clear();
|
||||||
|
markers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTrackPoint(TrackPoint trackPoint) {
|
||||||
|
this.trackPoints.add(trackPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTrackPoints(List<TrackPoint> trackPoints) {
|
||||||
|
this.trackPoints.addAll(trackPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMarker(Marker marker) {
|
||||||
|
this.markers.add(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMarkers(List<Marker> markers) {
|
||||||
|
this.markers.addAll(markers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTrack(Context context, String name, String uuid, String description, String category, String icon) {
|
||||||
|
track = new Track();
|
||||||
|
track.setName(name != null ? name : "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
track.setUuid(UUID.fromString(uuid));
|
||||||
|
} catch (IllegalArgumentException | NullPointerException e) {
|
||||||
|
Log.w(TAG, "could not parse Track UUID, generating a new one.");
|
||||||
|
track.setUuid(UUID.randomUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
track.setDescription(description != null ? description : "");
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
track.setCategory(category);
|
||||||
|
|
||||||
|
if (icon == null) {
|
||||||
|
icon = TrackIconUtils.getIconValue(context, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track.setIcon(icon != null ? icon : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
if (track != null) {
|
||||||
|
finishTrack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishTrack() {
|
||||||
|
if (trackPoints.isEmpty()) {
|
||||||
|
throw new ImportParserException("Cannot import track without any locations.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store Track
|
||||||
|
if (contentProviderUtils.getTrack(track.getUuid()) != null) {
|
||||||
|
if (preventReimport) {
|
||||||
|
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO This is a workaround until we have proper UI.
|
||||||
|
track.setUuid(UUID.randomUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(trackPoints, new Comparator<TrackPoint>() {
|
||||||
|
@Override
|
||||||
|
public int compare(TrackPoint o1, TrackPoint o2) {
|
||||||
|
if (o1.getTime().isBefore(o2.getTime())) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (o1.getTime().isAfter(o2.getTime())) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||||
|
updater.addTrackPoints(trackPoints, recordingDistanceInterval);
|
||||||
|
track.setTrackStatistics(updater.getTrackStatistics());
|
||||||
|
|
||||||
|
Uri trackUri = contentProviderUtils.insertTrack(track);
|
||||||
|
Track.Id trackId = new Track.Id(ContentUris.parseId(trackUri));
|
||||||
|
|
||||||
|
// Store TrackPoints
|
||||||
|
adjustTrackPoints();
|
||||||
|
contentProviderUtils.bulkInsertTrackPoint(trackPoints, trackId);
|
||||||
|
|
||||||
|
// Store Markers
|
||||||
|
matchMarkers2TrackPoints(trackId);
|
||||||
|
for (Marker marker : markers)
|
||||||
|
marker.setTrackId(trackId); //TODO Should happen in bulkInsertMarkers
|
||||||
|
|
||||||
|
contentProviderUtils.bulkInsertMarkers(markers, trackId);
|
||||||
|
|
||||||
|
//Clear up.
|
||||||
|
trackPoints.clear();
|
||||||
|
markers.clear();
|
||||||
|
|
||||||
|
trackIds.add(trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If not present: calculate data from the previous trackPoint (if present)
|
||||||
|
* NOTE: Modifies content of trackPoints.
|
||||||
|
*/
|
||||||
|
private void adjustTrackPoints() {
|
||||||
|
for (int i = 0; i < trackPoints.size(); i++) {
|
||||||
|
TrackPoint current = trackPoints.get(i);
|
||||||
|
|
||||||
|
if (current.hasLocation()) {
|
||||||
|
Instant time = current.getTime();
|
||||||
|
if (current.getLatitude() == 100) {
|
||||||
|
//TODO Remove by 31st December 2021.
|
||||||
|
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, time));
|
||||||
|
} else if (current.getLatitude() == 200) {
|
||||||
|
//TODO Remove by 31st December 2021.
|
||||||
|
trackPoints.set(i, new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time));
|
||||||
|
//TODO Delete location
|
||||||
|
} else if (!LocationUtils.isValidLocation(current.getLocation())) {
|
||||||
|
throw new ImportParserException("Invalid location detected: " + current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < trackPoints.size(); i++) {
|
||||||
|
TrackPoint previous = trackPoints.get(i - 1);
|
||||||
|
TrackPoint current = trackPoints.get(i);
|
||||||
|
|
||||||
|
Distance distanceToPrevious = current.distanceToPrevious(previous);
|
||||||
|
if (distanceToPrevious != null) {
|
||||||
|
if (!current.hasSpeed()) {
|
||||||
|
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
|
||||||
|
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current.hasBearing()) {
|
||||||
|
current.setBearing(previous.bearingTo(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||||
|
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: Modifies content of markers (incl. removal).
|
||||||
|
*/
|
||||||
|
private void matchMarkers2TrackPoints(Track.Id trackId) {
|
||||||
|
List<TrackPoint> trackPointsWithLocation = trackPoints.stream()
|
||||||
|
.filter(TrackPoint::hasLocation)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<Marker> todoMarkers = new LinkedList<>(markers);
|
||||||
|
List<Marker> doneMarkers = new LinkedList<>();
|
||||||
|
|
||||||
|
for (final TrackPoint trackPoint : trackPointsWithLocation) {
|
||||||
|
if (todoMarkers.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
|
||||||
|
updater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||||
|
|
||||||
|
List<Marker> matchedMarkers = todoMarkers.stream()
|
||||||
|
.filter(it -> trackPoint.getLatitude() == it.getLatitude()
|
||||||
|
&& trackPoint.getLongitude() == it.getLongitude()
|
||||||
|
&& trackPoint.getTime().equals(it.getTime())
|
||||||
|
)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
TrackStatistics statistics = updater.getTrackStatistics();
|
||||||
|
for (Marker marker : matchedMarkers) {
|
||||||
|
if (marker.hasPhoto()) {
|
||||||
|
marker.setPhotoUrl(getInternalPhotoUrl(trackId, marker.getPhotoUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
marker.setIcon(context.getString(R.string.marker_icon_url)); //TODO Why?
|
||||||
|
|
||||||
|
marker.setLength(statistics.getTotalDistance());
|
||||||
|
marker.setDuration(statistics.getTotalTime());
|
||||||
|
|
||||||
|
marker.setTrackPoint(trackPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
todoMarkers.removeAll(matchedMarkers);
|
||||||
|
doneMarkers.addAll(matchedMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todoMarkers.isEmpty()) {
|
||||||
|
Log.w(TAG, "Some markers could not be attached to TrackPoints; those are not imported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
markers.clear();
|
||||||
|
markers.addAll(doneMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the photo url for a file.
|
||||||
|
*
|
||||||
|
* @param externalPhotoUrl the file name
|
||||||
|
*/
|
||||||
|
private String getInternalPhotoUrl(@NonNull Track.Id trackId, @NonNull String externalPhotoUrl) {
|
||||||
|
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
|
||||||
|
File file = FileUtils.buildInternalPhotoFile(context, trackId, Uri.parse(importFileName));
|
||||||
|
if (file != null) {
|
||||||
|
Uri photoUri = FileUtils.getUriForFile(context, file);
|
||||||
|
return "" + photoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Track.Id> getTrackIds() {
|
||||||
|
return Collections.unmodifiableList(trackIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanImport() {
|
||||||
|
contentProviderUtils.deleteTracks(context, trackIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class XMLImporter {
|
|||||||
try {
|
try {
|
||||||
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
|
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
|
||||||
return parser.getImportTrackIds();
|
return parser.getImportTrackIds();
|
||||||
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
|
} catch (SAXException | ParserConfigurationException | ParsingException e) {
|
||||||
Log.e(TAG, "Unable to import file", e);
|
Log.e(TAG, "Unable to import file", e);
|
||||||
if (parser.getImportTrackIds().size() > 0) {
|
if (parser.getImportTrackIds().size() > 0) {
|
||||||
parser.cleanImport();
|
parser.cleanImport();
|
||||||
@@ -58,6 +58,7 @@ public class XMLImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TrackParser {
|
interface TrackParser {
|
||||||
|
@Deprecated
|
||||||
DefaultHandler getHandler();
|
DefaultHandler getHandler();
|
||||||
|
|
||||||
List<Track.Id> getImportTrackIds();
|
List<Track.Id> getImportTrackIds();
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package de.dennisguse.opentracks.services;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
|
||||||
|
public class TrackDeleteService extends Service {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = TrackDeleteService.class.getSimpleName();
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
static final String EXTRA_TRACK_IDS = "extra_track_ids";
|
||||||
|
|
||||||
|
private final Binder binder = new Binder();
|
||||||
|
private ExecutorService serviceExecutor;
|
||||||
|
private MutableLiveData<DeleteStatus> deleteResultObservable;
|
||||||
|
private NotificationManager notificationManager;
|
||||||
|
private NotificationCompat.Builder notificationBuilder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
serviceExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
deleteResultObservable = new MutableLiveData<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (serviceExecutor != null) {
|
||||||
|
serviceExecutor.shutdownNow();
|
||||||
|
serviceExecutor = null;
|
||||||
|
}
|
||||||
|
deleteResultObservable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||||
|
createAndShowNotification(trackIds.size());
|
||||||
|
deleteTracks(trackIds);
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
|
||||||
|
sendResult(null,0, trackIds.size());
|
||||||
|
serviceExecutor.execute(() -> {
|
||||||
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
for (int i = 0; i < trackIds.size(); i++) {
|
||||||
|
updateNotification(i + 1, trackIds.size());
|
||||||
|
contentProviderUtils.deleteTrack(this, trackIds.get(i));
|
||||||
|
sendResult(trackIds.get(i), i + 1, trackIds.size());
|
||||||
|
}
|
||||||
|
stopSelf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResult(Track.Id trackId, int deletes, int total) {
|
||||||
|
if (deleteResultObservable != null) {
|
||||||
|
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<DeleteStatus> getDeletingStatusObservable() {
|
||||||
|
return deleteResultObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts and shows the notification.
|
||||||
|
*
|
||||||
|
* @param tracksToDelete number of tracks to be deleted.
|
||||||
|
*/
|
||||||
|
private void createAndShowNotification(int tracksToDelete) {
|
||||||
|
notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, this.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
notificationChannel.setAllowBubbles(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
|
||||||
|
notificationBuilder
|
||||||
|
.setContentTitle(this.getString(R.string.track_delete_progress_message))
|
||||||
|
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
|
||||||
|
.setSmallIcon(R.drawable.ic_logo_color_24dp)
|
||||||
|
.setProgress(tracksToDelete, 0, false);
|
||||||
|
|
||||||
|
startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates notification progress.
|
||||||
|
*
|
||||||
|
* @param progress number of tracks already deleted.
|
||||||
|
* @param total total of tracks to be deleted.
|
||||||
|
*/
|
||||||
|
private void updateNotification(int progress, int total) {
|
||||||
|
notificationBuilder.setProgress(total, progress, false);
|
||||||
|
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Binder extends android.os.Binder {
|
||||||
|
|
||||||
|
private Binder() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackDeleteService getService() {
|
||||||
|
return TrackDeleteService.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DeleteStatus {
|
||||||
|
private int progress;
|
||||||
|
private int max;
|
||||||
|
private Track.Id trackId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param trackId Track.Id just deleted if any.
|
||||||
|
* @param progress number of deletes.
|
||||||
|
* @param max total of deletes to be done.
|
||||||
|
*/
|
||||||
|
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.progress = progress;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFinished() {
|
||||||
|
return progress == max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDeleted(Track.Id trackId) {
|
||||||
|
return this.trackId != null && this.trackId.equals(trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DeleteStatus{" +
|
||||||
|
"number of deletes=" + progress +
|
||||||
|
", total=" + max +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.dennisguse.opentracks.services;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.BuildConfig;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
|
||||||
|
import static de.dennisguse.opentracks.services.TrackDeleteService.EXTRA_TRACK_IDS;
|
||||||
|
|
||||||
|
public class TrackDeleteServiceConnection implements ServiceConnection {
|
||||||
|
|
||||||
|
final private Listener listener;
|
||||||
|
private TrackDeleteService trackDeleteService;
|
||||||
|
|
||||||
|
public TrackDeleteServiceConnection(@NonNull Listener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
trackDeleteService = ((TrackDeleteService.Binder) service).getService();
|
||||||
|
listener.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
trackDeleteService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAndBind(Context context, ArrayList<Track.Id> trackIds) {
|
||||||
|
if (trackDeleteService != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, TrackDeleteService.class)
|
||||||
|
.putParcelableArrayListExtra(EXTRA_TRACK_IDS, trackIds);
|
||||||
|
context.startService(intent);
|
||||||
|
|
||||||
|
bind(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(Context context) {
|
||||||
|
if (trackDeleteService != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.bindService(new Intent(context, TrackDeleteService.class), this, BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unbind(Context context) {
|
||||||
|
context.unbindService(this);
|
||||||
|
trackDeleteService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackDeleteService getServiceIfBound() {
|
||||||
|
return trackDeleteService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void connected();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,7 +76,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
|
|
||||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
||||||
|
|
||||||
public static final RecordingStatus STATUS_DEFAULT = new RecordingStatus(null, false);
|
public static final RecordingStatus STATUS_DEFAULT = new RecordingStatus(null, false);
|
||||||
|
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
|
||||||
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
|
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
|
||||||
|
|
||||||
// The following variables are set in onCreate:
|
// The following variables are set in onCreate:
|
||||||
@@ -84,6 +87,15 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
private PeriodicTaskExecutor voiceExecutor;
|
private PeriodicTaskExecutor voiceExecutor;
|
||||||
private TrackRecordingServiceNotificationManager notificationManager;
|
private TrackRecordingServiceNotificationManager notificationManager;
|
||||||
|
|
||||||
|
private Handler handler;
|
||||||
|
private final Runnable updateRecordingData = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateRecordingDataWhileRecording();
|
||||||
|
handler.postDelayed(this, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
private Distance recordingDistanceInterval;
|
private Distance recordingDistanceInterval;
|
||||||
@@ -127,14 +139,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
private RecordingStatus recordingStatus;
|
private RecordingStatus recordingStatus;
|
||||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||||
|
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
|
handler = new Handler();
|
||||||
|
|
||||||
recordingStatusObservable = new MutableLiveData<>();
|
recordingStatusObservable = new MutableLiveData<>();
|
||||||
updateRecordingStatus(STATUS_DEFAULT);
|
updateRecordingStatus(STATUS_DEFAULT);
|
||||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||||
|
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
|
||||||
|
|
||||||
handlerServer = new HandlerServer(this);
|
handlerServer = new HandlerServer(this);
|
||||||
|
|
||||||
@@ -160,6 +176,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
|
handler = null;
|
||||||
|
|
||||||
handlerServer.stop(this);
|
handlerServer.stop(this);
|
||||||
handlerServer = null;
|
handlerServer = null;
|
||||||
|
|
||||||
@@ -192,6 +210,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
|
|
||||||
recordingStatusObservable = null;
|
recordingStatusObservable = null;
|
||||||
gpsStatusObservable = null;
|
gpsStatusObservable = null;
|
||||||
|
recordingDataObservable = null;
|
||||||
|
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
@@ -208,6 +227,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
return recordingStatus.getTrackId();
|
return recordingStatus.getTrackId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
public TrackStatistics getTrackStatistics() {
|
public TrackStatistics getTrackStatistics() {
|
||||||
if (trackStatisticsUpdater == null) {
|
if (trackStatisticsUpdater == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -352,6 +372,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
remoteSensorManager = new BluetoothRemoteSensorManager(this);
|
remoteSensorManager = new BluetoothRemoteSensorManager(this);
|
||||||
remoteSensorManager.start();
|
remoteSensorManager.start();
|
||||||
|
|
||||||
|
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
||||||
|
|
||||||
altitudeSumManager = new AltitudeSumManager();
|
altitudeSumManager = new AltitudeSumManager();
|
||||||
altitudeSumManager.start(this);
|
altitudeSumManager.start(this);
|
||||||
|
|
||||||
@@ -438,6 +460,13 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
* @param trackStopped true if track is stopped, false if track is paused
|
* @param trackStopped true if track is stopped, false if track is paused
|
||||||
*/
|
*/
|
||||||
private void endRecording(boolean trackStopped) {
|
private void endRecording(boolean trackStopped) {
|
||||||
|
handler.removeCallbacks(updateRecordingData);
|
||||||
|
if (!trackStopped) {
|
||||||
|
updateRecordingDataWhileRecording();
|
||||||
|
} else {
|
||||||
|
recordingDataObservable.postValue(NOT_RECORDING);
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown periodic tasks
|
// Shutdown periodic tasks
|
||||||
voiceExecutor.shutdown();
|
voiceExecutor.shutdown();
|
||||||
|
|
||||||
@@ -613,14 +642,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
trackPoint.setAltitudeLoss(getAltitudeLoss_m());
|
trackPoint.setAltitudeLoss(getAltitudeLoss_m());
|
||||||
altitudeSumManager.reset();
|
altitudeSumManager.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO We need a copy of the SensorDataSet as data will be reset!
|
||||||
|
SensorDataSet sensorDataSet = fillWithSensorDataSet(trackPoint);
|
||||||
if (remoteSensorManager != null) {
|
if (remoteSensorManager != null) {
|
||||||
fillWithSensorDataSet(trackPoint);
|
|
||||||
remoteSensorManager.reset();
|
remoteSensorManager.reset();
|
||||||
}
|
}
|
||||||
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
|
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
|
||||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||||
|
|
||||||
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
||||||
|
|
||||||
|
recordingDataObservable.postValue(new RecordingData(track, trackPoint, sensorDataSet));
|
||||||
|
|
||||||
contentProviderUtils.updateTrack(track);
|
contentProviderUtils.updateTrack(track);
|
||||||
} catch (SQLiteException e) {
|
} catch (SQLiteException e) {
|
||||||
/*
|
/*
|
||||||
@@ -632,25 +665,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
voiceExecutor.update();
|
voiceExecutor.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SensorDataSet getSensorDataSet() {
|
private SensorDataSet fillWithSensorDataSet(TrackPoint trackPoint) {
|
||||||
if (remoteSensorManager == null) {
|
if (remoteSensorManager == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteSensorManager.getSensorData();
|
SensorDataSet sensorData = remoteSensorManager.getSensorData();
|
||||||
}
|
if (sensorData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void fillWithSensorDataSet(TrackPoint trackPoint) {
|
if (trackPoint != null) {
|
||||||
SensorDataSet sensorData = getSensorDataSet();
|
|
||||||
if (sensorData != null) {
|
|
||||||
sensorData.fillTrackPoint(trackPoint);
|
sensorData.fillTrackPoint(trackPoint);
|
||||||
}
|
}
|
||||||
|
return sensorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the relative altitude gain (since last trackpoint).
|
* Returns the relative altitude gain (since last trackpoint).
|
||||||
*/
|
*/
|
||||||
public Float getAltitudeGain_m() {
|
private Float getAltitudeGain_m() {
|
||||||
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -661,7 +695,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
/**
|
/**
|
||||||
* Returns the relative altitude loss (since last trackpoint).
|
* Returns the relative altitude loss (since last trackpoint).
|
||||||
*/
|
*/
|
||||||
public Float getAltitudeLoss_m() {
|
private Float getAltitudeLoss_m() {
|
||||||
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -712,6 +746,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
return gpsStatusObservable;
|
return gpsStatusObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MutableLiveData<RecordingData> getRecordingDataObservable() {
|
||||||
|
return recordingDataObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRecordingDataWhileRecording() {
|
||||||
|
if (!recordingStatus.isRecording()) {
|
||||||
|
Log.w(TAG, "Currently not recording; cannot update data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Track track = contentProviderUtils.getTrack(recordingStatus.getTrackId());
|
||||||
|
track.getTrackStatistics().setTotalTime(TrackRecordingService.this.getTotalTime());
|
||||||
|
|
||||||
|
SensorDataSet sensorDataSet = fillWithSensorDataSet(lastTrackPoint);
|
||||||
|
recordingDataObservable.postValue(new RecordingData(track, lastTrackPoint, sensorDataSet));
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<RecordingStatus> getRecordingStatusObservable() {
|
public LiveData<RecordingStatus> getRecordingStatusObservable() {
|
||||||
return recordingStatusObservable;
|
return recordingStatusObservable;
|
||||||
}
|
}
|
||||||
@@ -792,4 +842,50 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class RecordingData {
|
||||||
|
|
||||||
|
private final Track track;
|
||||||
|
|
||||||
|
private final TrackPoint latestTrackPoint;
|
||||||
|
|
||||||
|
private final SensorDataSet sensorDataSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Track} and {@link TrackPoint} must be immutable (i.e., their content does not change).
|
||||||
|
*/
|
||||||
|
public RecordingData(Track track, TrackPoint lastTrackPoint, SensorDataSet sensorDataSet) {
|
||||||
|
this.track = track;
|
||||||
|
this.latestTrackPoint = lastTrackPoint;
|
||||||
|
this.sensorDataSet = sensorDataSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Track getTrack() {
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTrackCategory() {
|
||||||
|
if (track == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return track.getCategory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public TrackStatistics getTrackStatistics() {
|
||||||
|
if (track == null) {
|
||||||
|
return new TrackStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.getTrackStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackPoint getLatestTrackPoint() {
|
||||||
|
return latestTrackPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SensorDataSet getSensorDataSet() {
|
||||||
|
return sensorDataSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
|||||||
|
|
||||||
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
|
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
|
||||||
|
|
||||||
|
@Deprecated //TODO Implement proper interface that passes the current TrackRecordingService
|
||||||
private final Runnable callback;
|
private final Runnable callback;
|
||||||
|
|
||||||
private TrackRecordingService trackRecordingService;
|
private TrackRecordingService trackRecordingService;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.util.Log;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||||
@@ -57,7 +58,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
private boolean started = false;
|
private boolean started = false;
|
||||||
|
|
||||||
private final SharedPreferences sharedPreferences;
|
private final SharedPreferences sharedPreferences;
|
||||||
private int preferenceWheelCircumference;
|
private Distance preferenceWheelCircumference;
|
||||||
|
|
||||||
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
|
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
|
||||||
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
||||||
|
|||||||
@@ -138,11 +138,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(@NonNull TrackRecordingService trackRecordingService) {
|
public void run(@NonNull TrackRecordingService trackRecordingService) {
|
||||||
if (trackRecordingService == null) {
|
|
||||||
Log.e(TAG, "TrackRecordingService is null.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
|
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +178,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
|||||||
}
|
}
|
||||||
String category = track.getCategory();
|
String category = track.getCategory();
|
||||||
|
|
||||||
//TODO Querying all TrackPoints all the time is inefficient; use TrackDataHub or something else.
|
//TODO Querying all TrackPoints all the time is inefficient; get infos from TrackRecordingService
|
||||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
||||||
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||||
while (trackPointIterator.hasNext()) {
|
while (trackPointIterator.hasNext()) {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.settings;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
@@ -93,6 +95,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
|
|
||||||
public static class PrefsFragment extends PreferenceFragmentCompat {
|
public static class PrefsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
private TextToSpeech tts;
|
||||||
|
private int ttsInitStatus;
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||||
@@ -125,6 +130,22 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
|
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
|
||||||
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
|
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
|
||||||
|
|
||||||
|
private Preference voiceAnnouncements;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
ttsInitStatus = TextToSpeech.ERROR;
|
||||||
|
tts = new TextToSpeech(getContext(), status -> {
|
||||||
|
ttsInitStatus = status;
|
||||||
|
Log.i(TAG, "TextToSpeech initialized with status " + status);
|
||||||
|
|
||||||
|
if (voiceAnnouncements != null) {
|
||||||
|
updateVoiceAnnouncements();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||||
@@ -150,6 +171,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
//Use same value for not set as Androidx ListPreference and EditTextPreference
|
//Use same value for not set as Androidx ListPreference and EditTextPreference
|
||||||
return directory != null ? directory.getName() : getString(R.string.not_set);
|
return directory != null ? directory.getName() : getString(R.string.not_set);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
voiceAnnouncements = findPreference(getString(R.string.voice_frequency_key));
|
||||||
|
updateVoiceAnnouncements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -208,6 +232,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
trackRecordingServiceConnection.unbind(getContext());
|
trackRecordingServiceConnection.unbind(getContext());
|
||||||
sharedPreferences = null;
|
sharedPreferences = null;
|
||||||
|
voiceAnnouncements = null;
|
||||||
|
tts = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultActivity(String iconValue) {
|
public void setDefaultActivity(String iconValue) {
|
||||||
@@ -296,6 +322,10 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateVoiceAnnouncements() {
|
||||||
|
voiceAnnouncements.setEnabled(ttsInitStatus == TextToSpeech.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||||
this.recordingStatus = status;
|
this.recordingStatus = status;
|
||||||
if (!status.isRecording() && isAdded()) {
|
if (!status.isRecording() && isAdded()) {
|
||||||
@@ -303,5 +333,4 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Distance;
|
import de.dennisguse.opentracks.content.data.Distance;
|
||||||
import de.dennisguse.opentracks.content.data.Speed;
|
import de.dennisguse.opentracks.content.data.Speed;
|
||||||
@@ -98,10 +99,13 @@ public class TrackStatisticsUpdater {
|
|||||||
return trackInitialized;
|
return trackInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addTrackPoints(List<TrackPoint> trackPoints, Distance minGPSDistance) {
|
||||||
|
for (TrackPoint tp : trackPoints) {
|
||||||
|
addTrackPoint(tp, minGPSDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a trackPoint.
|
|
||||||
*
|
|
||||||
* @param trackPoint the trackPoint
|
|
||||||
* @param minGPSDistance the min recording distance
|
* @param minGPSDistance the min recording distance
|
||||||
*/
|
*/
|
||||||
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
|
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
|
||||||
@@ -159,7 +163,7 @@ public class TrackStatisticsUpdater {
|
|||||||
|
|
||||||
if (!trackPoint.hasSensorDistance()) {
|
if (!trackPoint.hasSensorDistance()) {
|
||||||
// GPS-based distance/speed
|
// GPS-based distance/speed
|
||||||
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
|
Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
|
||||||
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
|
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
|
||||||
speedBuffer_mps.reset();
|
speedBuffer_mps.reset();
|
||||||
lastTrackPoint = trackPoint;
|
lastTrackPoint = trackPoint;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.ContextualActionModeCallback;
|
import de.dennisguse.opentracks.ContextualActionModeCallback;
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TrackController;
|
|
||||||
|
|
||||||
import static android.content.Context.VIBRATOR_SERVICE;
|
import static android.content.Context.VIBRATOR_SERVICE;
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ public class ActivityUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
|
public static SearchView configureSearchWidget(Activity activity, final MenuItem menuItem) {
|
||||||
final SearchView searchView = (SearchView) menuItem.getActionView();
|
final SearchView searchView = (SearchView) menuItem.getActionView();
|
||||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||||
if (searchManager != null) {
|
if (searchManager != null) {
|
||||||
@@ -97,47 +96,12 @@ public class ActivityUtils {
|
|||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||||
}
|
}
|
||||||
searchView.setQueryRefinementEnabled(true);
|
|
||||||
searchView.setSubmitButtonEnabled(true);
|
searchView.setSubmitButtonEnabled(true);
|
||||||
searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
|
return searchView;
|
||||||
// Hide and show trackController when searchable widget has focus/no focus
|
|
||||||
if (trackController != null) {
|
|
||||||
if (hasFocus) {
|
|
||||||
trackController.hide();
|
|
||||||
} else {
|
|
||||||
trackController.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String query) {
|
|
||||||
menuItem.collapseActionView();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String newText) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onSuggestionSelect(int position) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSuggestionClick(int position) {
|
|
||||||
menuItem.collapseActionView();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void vibrate(Activity activity, int milliseconds) {
|
public static void vibrate(Context context, int milliseconds) {
|
||||||
final Vibrator vibrator = (Vibrator) activity.getSystemService(VIBRATOR_SERVICE);
|
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
|
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ public class PreferencesUtils {
|
|||||||
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
|
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
|
public static Distance getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
|
||||||
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
|
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
|
||||||
return getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
|
return Distance.ofMM(getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) {
|
public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_logo_color_108dp" />
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:inset="20dp"
|
||||||
|
android:drawable="@drawable/ic_logo_color_24dp" />
|
||||||
|
</foreground>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/ic_launcher_background" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_logo_color_24dp"
|
||||||
|
android:inset="8dp" />
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<group
|
|
||||||
android:scaleX="0.56"
|
|
||||||
android:scaleY="0.56"
|
|
||||||
android:translateX="21.50"
|
|
||||||
android:translateY="24.00">
|
|
||||||
<path
|
|
||||||
android:pathData="M85.133,20.59C85.731,18.546 84.756,17.817 82.965,18.968L23.874,56.967c-1.791,1.152 -1.519,2.224 0.604,2.382L49.019,61.18c2.124,0.159 4.477,1.918 5.229,3.91l8.7,23.021c0.753,1.992 1.857,1.949 2.455,-0.094z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#f69123">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="84.4913"
|
|
||||||
android:endY="19.211866"
|
|
||||||
android:startX="38.773293"
|
|
||||||
android:startY="80.33152"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#FFFBB22F"
|
|
||||||
android:offset="0" />
|
|
||||||
<item
|
|
||||||
android:color="#FFF58820"
|
|
||||||
android:offset="1" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -8,8 +8,11 @@
|
|||||||
<item
|
<item
|
||||||
android:width="300dp"
|
android:width="300dp"
|
||||||
android:height="300dp"
|
android:height="300dp"
|
||||||
android:drawable="@drawable/ic_logo_color_108dp"
|
android:gravity="center"
|
||||||
android:gravity="center_vertical|center_horizontal"
|
tools:targetApi="m">
|
||||||
tools:targetApi="m" />
|
<inset
|
||||||
|
android:drawable="@drawable/ic_logo_color_24dp"
|
||||||
|
android:inset="20dp" />
|
||||||
|
</item>
|
||||||
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Copyright 2013 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/controller_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/controller_background"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/controller_record"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/image_record"
|
||||||
|
android:src="@drawable/ic_button_record" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="6">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/controller_total_time"
|
||||||
|
style="@style/TextSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/controller_status"
|
||||||
|
style="@style/TextMicro"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/controller_total_time"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:singleLine="true" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/controller_stop"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/image_stop"
|
||||||
|
android:src="@drawable/button_stop" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Copyright 2013 Google Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/track_controller_container"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@drawable/track_controller_background"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingEnd="8dp">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/track_controller_record"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="10"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/image_record"
|
|
||||||
android:src="@drawable/ic_button_record" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="6">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/track_controller_total_time"
|
|
||||||
style="@style/TextSmall"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:textColor="@android:color/white" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/track_controller_status"
|
|
||||||
style="@style/TextMicro"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_above="@id/track_controller_total_time"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:singleLine="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/track_controller_stop"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="10"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/image_stop"
|
|
||||||
android:src="@drawable/button_stop" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</merge>
|
|
||||||
@@ -27,9 +27,11 @@ limitations under the License.
|
|||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<include
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/track_controller_container"
|
android:id="@+id/controller_fragment"
|
||||||
layout="@layout/track_controller" />
|
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/track_list"
|
android:id="@+id/track_list"
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<include
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/track_controller_container"
|
android:id="@+id/controller_fragment"
|
||||||
layout="@layout/track_controller" />
|
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/chart_view_layout"
|
<de.dennisguse.opentracks.chart.ChartView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/chart_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent" />
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Copyright 2012 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/controller_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/controller_background"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/controller_record"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/image_record"
|
||||||
|
android:src="@drawable/ic_button_record" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_weight="6">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/controller_total_time"
|
||||||
|
style="@style/TextSmall"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
tools:text="0:12:34" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/controller_status"
|
||||||
|
style="@style/TextMicro"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_above="@id/controller_total_time"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:text="@string/generic_recording" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/controller_stop"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/image_stop"
|
||||||
|
android:src="@drawable/button_stop" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
android:id="@+id/stats_altitude_group"
|
android:id="@+id/stats_altitude_group"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit" />
|
app:constraint_referenced_ids="stats_altitude_horizontal_line,stats_altitude_current_label,stats_altitude_current_value,stats_altitude_current_unit,stats_altitude_gain_label,stats_altitude_gain_value,stats_altitude_gain_unit,stats_altitude_loss_label,stats_altitude_loss_value,stats_altitude_loss_unit,stats_elevation_current_label_egm" />
|
||||||
|
|
||||||
<!-- altitude -->
|
<!-- altitude -->
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Copyright 2012 Google Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/track_controller_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/track_controller_background"
|
|
||||||
android:paddingTop="8dp"
|
|
||||||
android:paddingBottom="8dp">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/track_controller_record"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="10"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/image_record"
|
|
||||||
android:src="@drawable/ic_button_record" />
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_weight="6">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/track_controller_total_time"
|
|
||||||
style="@style/TextSmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
tools:text="0:12:34" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/track_controller_status"
|
|
||||||
style="@style/TextMicro"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_above="@id/track_controller_total_time"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:singleLine="true"
|
|
||||||
tools:text="@string/generic_recording" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/track_controller_stop"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="10"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/image_stop"
|
|
||||||
android:src="@drawable/button_stop" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</merge>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progressbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minHeight="100dp"
|
|
||||||
android:minWidth="100dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -83,7 +83,9 @@ limitations under the License.
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<include
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/track_controller_container"
|
android:id="@+id/controller_fragment"
|
||||||
layout="@layout/track_controller" />
|
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<include
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/track_controller_container"
|
android:id="@+id/controller_fragment"
|
||||||
layout="@layout/track_controller" />
|
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -20,14 +20,14 @@ limitations under the License.
|
|||||||
android:icon="@drawable/ic_statistics_24dp"
|
android:icon="@drawable/ic_statistics_24dp"
|
||||||
android:title="@string/menu_aggregated_statistics"
|
android:title="@string/menu_aggregated_statistics"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
<!-- Icon and title is overwritten by TrackListActivity -->
|
<!-- Icon, title, and visibility is managed by TrackListActivity -->
|
||||||
<!-- Hide icon as workaround for https://github.com/OpenTracksApp/OpenTracks/issues/200 -->
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/track_list_start_gps"
|
android:id="@+id/track_list_start_gps"
|
||||||
android:icon="@drawable/ic_gps_off_24dp"
|
android:icon="@drawable/ic_gps_off_24dp"
|
||||||
android:title="@string/menu_start_gps"
|
android:title="@string/menu_start_gps"
|
||||||
android:visible="false"
|
android:visible="false"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
|
||||||
<item
|
<item
|
||||||
android:id="@+id/track_list_search"
|
android:id="@+id/track_list_search"
|
||||||
android:title="@string/menu_search"
|
android:title="@string/menu_search"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -66,12 +66,12 @@
|
|||||||
<string name="activity_type_walking">caminata</string>
|
<string name="activity_type_walking">caminata</string>
|
||||||
<string name="activity_type_water_skiing">esquí acuático</string>
|
<string name="activity_type_water_skiing">esquí acuático</string>
|
||||||
<string name="activity_type_wind_surfing">windsurf</string>
|
<string name="activity_type_wind_surfing">windsurf</string>
|
||||||
<string name="description_activity_type">Tipo de actividad: %1$s.</string>
|
<string name="description_activity_type">Tipo de actividad: %1$s</string>
|
||||||
<string name="description_average_moving_pace_in_minute">Velocidad promedio de movimiento: %1$s min/km (%2$s min/mi)</string>
|
<string name="description_average_moving_pace_in_minute">Velocidad promedio de movimiento: %1$s min/km (%2$s min/mi)</string>
|
||||||
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h).</string>
|
<string name="description_average_moving_speed">Velocidad promedio de movimiento: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||||
<string name="description_average_pace_in_minute">Velocidad promedio: %1$s min/km (%2$s min/mi)</string>
|
<string name="description_average_pace_in_minute">Velocidad promedio: %1$s min/km (%2$s min/mi)</string>
|
||||||
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h).</string>
|
<string name="description_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||||
<string name="description_altitude_gain">Altitud: %1$d m (%2$d pies).</string>
|
<string name="description_altitude_gain">Variación de altitud: %1$d m (%2$d pies)</string>
|
||||||
<string name="description_altitude_imperial">Elevación (pies)</string>
|
<string name="description_altitude_imperial">Elevación (pies)</string>
|
||||||
<string name="description_altitude_metric">Elevación (m)</string>
|
<string name="description_altitude_metric">Elevación (m)</string>
|
||||||
<string name="description_fastest_pace_in_minute">Velocidad más rápida: %1$s min/km (%2$s min/mi)</string>
|
<string name="description_fastest_pace_in_minute">Velocidad más rápida: %1$s min/km (%2$s min/mi)</string>
|
||||||
@@ -81,31 +81,31 @@
|
|||||||
<string name="description_location_latitude">Latitud (grados)</string>
|
<string name="description_location_latitude">Latitud (grados)</string>
|
||||||
<string name="description_location_longitude">Longitud (grados)</string>
|
<string name="description_location_longitude">Longitud (grados)</string>
|
||||||
<string name="description_location_speed">Velocidad (m/s)</string>
|
<string name="description_location_speed">Velocidad (m/s)</string>
|
||||||
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies).</string>
|
<string name="description_max_altitude">Elevación máx.: %1$d m (%2$d pies)</string>
|
||||||
<string name="description_max_grade">Pendiente máx.: %1$d %%.</string>
|
<string name="description_max_grade">Pendiente máx.: %1$d %%</string>
|
||||||
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h).</string>
|
<string name="description_max_speed">Velocidad máx.: %1$.2f km/h (%2$.1f mi/h)</string>
|
||||||
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies).</string>
|
<string name="description_min_altitude">Elevación mín.: %1$d m (%2$d pies)</string>
|
||||||
<string name="description_min_grade">Pendiente mín.: %1$d %%.</string>
|
<string name="description_min_grade">Pendiente mín.: %1$d %%</string>
|
||||||
<string name="description_moving_time">Tiempo en movimiento: %1$s.</string>
|
<string name="description_moving_time">Tiempo en movimiento: %1$s</string>
|
||||||
<string name="description_pace_imperial">Velocidad (min/mi)</string>
|
<string name="description_pace_imperial">Velocidad (min/mi)</string>
|
||||||
<string name="description_pace_metric">Velocidad (min/km)</string>
|
<string name="description_pace_metric">Velocidad (min/km)</string>
|
||||||
<string name="description_recorded_time">Registrado: %1$s.</string>
|
<string name="description_recorded_time">Registrado: %1$s</string>
|
||||||
<string name="description_sensor_cadence">Cadencia (rpm)</string>
|
<string name="description_sensor_cadence">Cadencia (rpm)</string>
|
||||||
<string name="description_sensor_heart_rate">Frecuencia cardíaca (lpm)</string>
|
<string name="description_sensor_heart_rate">Frecuencia cardíaca (lpm)</string>
|
||||||
<string name="description_sensor_power">Potencia (W)</string>
|
<string name="description_sensor_power">Potencia (W)</string>
|
||||||
<string name="description_speed_imperial">Velocidad (mi/h)</string>
|
<string name="description_speed_imperial">Velocidad (mi/h)</string>
|
||||||
<string name="description_speed_metric">Velocidad (km/h)</string>
|
<string name="description_speed_metric">Velocidad (km/h)</string>
|
||||||
<string name="description_time">Tiempo</string>
|
<string name="description_time">Tiempo</string>
|
||||||
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi).</string>
|
<string name="description_total_distance">Distancia total: %1$.2f km (%2$.1f mi)</string>
|
||||||
<string name="description_total_time">Tiempo total: %1$s.</string>
|
<string name="description_total_time">Tiempo total: %1$s</string>
|
||||||
<string name="description_track_point">Punto</string>
|
<string name="description_track_point">Punto</string>
|
||||||
<string name="description_track_segment">Segmento</string>
|
<string name="description_track_segment">Segmento</string>
|
||||||
<string name="export_all_option">Todo como %1$s</string>
|
<string name="export_all_option">Todo como %1$s</string>
|
||||||
<string name="export_all_title">Exportar a unidad de almacenamiento externa</string>
|
<string name="export_all_title">Exportar a unidad de almacenamiento externa</string>
|
||||||
<string name="export_error">Se exportaron %1$d de %2$s a %3$s.</string>
|
<string name="export_error">Se exportaron %1$d de %2$s a %3$s</string>
|
||||||
<string name="export_option">como %1$s a %2$s</string>
|
<string name="export_option">como %1$s a %2$s</string>
|
||||||
<string name="export_progress_message">Exportando a %1$s…</string>
|
<string name="export_progress_message">Exportando a %1$s…</string>
|
||||||
<string name="export_success">Se exportaron %1$s a %2$s.</string>
|
<string name="export_success">Se exportaron %1$s a %2$s</string>
|
||||||
<string name="export_title">Exportar a…</string>
|
<string name="export_title">Exportar a…</string>
|
||||||
<string name="files_few">%1$d archivos</string>
|
<string name="files_few">%1$d archivos</string>
|
||||||
<string name="files_many">%1$d archivos</string>
|
<string name="files_many">%1$d archivos</string>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<string name="gps_location_access">“Acceso a ubicaciones”</string>
|
<string name="gps_location_access">“Acceso a ubicaciones”</string>
|
||||||
<string name="gps_disabled">El GPS no está habilitado. Puntea aquí para ir a %1$s. Para volver, presiona el botón Atrás.</string>
|
<string name="gps_disabled">El GPS no está habilitado. Puntea aquí para ir a %1$s. Para volver, presiona el botón Atrás.</string>
|
||||||
<string name="gps_disabled_my_location">No se puede determinar la ubicación actual. Habilita más fuentes de ubicación en %1$s y asegúrate de que se pueda ver el cielo. Vuelve a intentarlo más tarde.</string>
|
<string name="gps_disabled_my_location">No se puede determinar la ubicación actual. Habilita más fuentes de ubicación en %1$s y asegúrate de que se pueda ver el cielo. Vuelve a intentarlo más tarde.</string>
|
||||||
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo.</string>
|
<string name="gps_not_found">No se encontró ningún GPS en el dispositivo</string>
|
||||||
<string name="gps_starting">Iniciando GPS</string>
|
<string name="gps_starting">Iniciando GPS</string>
|
||||||
<string name="gps_wait_for_better_signal">Esperando una mejor señal de GPS</string>
|
<string name="gps_wait_for_better_signal">Esperando una mejor señal de GPS</string>
|
||||||
<string name="gps_wait_for_signal">Esperando señal de GPS</string>
|
<string name="gps_wait_for_signal">Esperando señal de GPS</string>
|
||||||
@@ -148,21 +148,21 @@
|
|||||||
<string name="image_record">Grabar</string>
|
<string name="image_record">Grabar</string>
|
||||||
<string name="image_stop">Parar</string>
|
<string name="image_stop">Parar</string>
|
||||||
<string name="image_track">Seguir</string>
|
<string name="image_track">Seguir</string>
|
||||||
<string name="import_error">Se importaron %1$d de %2$s desde %3$s.</string>
|
<string name="import_error">Se importaron %1$d de %2$s desde %3$s</string>
|
||||||
<string name="import_no_directory">%1$s no existe.</string>
|
<string name="import_no_directory">%1$s no existe</string>
|
||||||
<string name="import_no_file">No hay archivos para importar. Conecta la unidad a la computadora y pasa los archivos que deseas importar a %1$s.</string>
|
<string name="import_no_file">No hay archivos para importar. Conecta la unidad a la computadora y pasa los archivos que deseas importar a %1$s.</string>
|
||||||
<string name="import_no_file_title">Importar archivos</string>
|
<string name="import_no_file_title">Importar archivos</string>
|
||||||
<string name="import_progress_message">Importando desde %1$s…</string>
|
<string name="import_progress_message">Importando desde %1$s…</string>
|
||||||
<string name="import_selection_option">Todos los archivos %1$s</string>
|
<string name="import_selection_option">Todos los archivos %1$s</string>
|
||||||
<string name="import_selection_title">Importar desde la unidad de almacenamiento externa</string>
|
<string name="import_selection_title">Importar desde la unidad de almacenamiento externa</string>
|
||||||
<string name="import_success">Se importaron %1$s desde %2$s.</string>
|
<string name="import_success">Se importaron %1$s desde %2$s</string>
|
||||||
<string name="marker_add_error">No se puede insertar un marcador. No se recibe señal de GPS. Vuelve a intentarlo.</string>
|
<string name="marker_add_error">No se puede insertar un marcador. No se recibe señal de GPS. Vuelve a intentarlo.</string>
|
||||||
<string name="marker_add_success">Se insertó un marcador.</string>
|
<string name="marker_add_success">Se insertó un marcador</string>
|
||||||
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador.</string>
|
<string name="marker_add_canceled">Cancelado, no se agregó ningún marcador</string>
|
||||||
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente.</string>
|
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente</string>
|
||||||
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente.</string>
|
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente</string>
|
||||||
<string name="marker_delete_one_confirm_title">¿Eliminar marcador?</string>
|
<string name="marker_delete_one_confirm_title">¿Eliminar marcador?</string>
|
||||||
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente.</string>
|
<string name="marker_delete_multiple_confirm_message">Los marcadores seleccionados se eliminarán del dispositivo de forma permanente</string>
|
||||||
<string name="marker_label_end">%1$s (Fin)</string>
|
<string name="marker_label_end">%1$s (Fin)</string>
|
||||||
<string name="marker_label_start">%1$s (Inicio)</string>
|
<string name="marker_label_start">%1$s (Inicio)</string>
|
||||||
<string name="marker_name_format">Marcador %1$d</string>
|
<string name="marker_name_format">Marcador %1$d</string>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
<string name="marker_detail_statistics_title">Desde el último marcador de división:</string>
|
<string name="marker_detail_statistics_title">Desde el último marcador de división:</string>
|
||||||
<string name="marker_edit_marker_type">Tipo de marcador (p. ej., cascada)</string>
|
<string name="marker_edit_marker_type">Tipo de marcador (p. ej., cascada)</string>
|
||||||
<string name="marker_edit_marker_type_hint">Tipo de marcador</string>
|
<string name="marker_edit_marker_type_hint">Tipo de marcador</string>
|
||||||
<string name="marker_list_empty_message">No tienes ningún marcador.</string>
|
<string name="marker_list_empty_message">No tienes ningún marcador</string>
|
||||||
<string name="menu_aggregated_statistics">Estadíst. globales</string>
|
<string name="menu_aggregated_statistics">Estadíst. globales</string>
|
||||||
<string name="menu_delete">Eliminar</string>
|
<string name="menu_delete">Eliminar</string>
|
||||||
<string name="menu_edit">Editar</string>
|
<string name="menu_edit">Editar</string>
|
||||||
@@ -196,17 +196,17 @@
|
|||||||
<string name="menu_resume_track">Reanudar ruta</string>
|
<string name="menu_resume_track">Reanudar ruta</string>
|
||||||
<string name="recording_service">Servicio de grabación</string>
|
<string name="recording_service">Servicio de grabación</string>
|
||||||
<string name="search_global_description">Recorridos y marcadores</string>
|
<string name="search_global_description">Recorridos y marcadores</string>
|
||||||
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda.</string>
|
<string name="search_list_empty_message">No hay rutas ni marcadores que coincidan con tu búsqueda</string>
|
||||||
<string name="search_list_marker_track_location">En el recorrido “%1$s”</string>
|
<string name="search_list_marker_track_location">En el recorrido “%1$s”.</string>
|
||||||
<string name="sensor_state_battery">Nivel de la batería</string>
|
<string name="sensor_state_battery">Nivel de la batería</string>
|
||||||
<string name="sensor_state_cadence">Cadencia</string>
|
<string name="sensor_state_cadence">Cadencia</string>
|
||||||
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
||||||
<string name="sensor_state_disconnected">Desconectado</string>
|
<string name="sensor_state_disconnected">Desconectado</string>
|
||||||
<string name="sensor_state_heart_rate">Frecuencia cardíaca</string>
|
<string name="sensor_state_heart_rate">Frecuencia cardíaca</string>
|
||||||
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
|
<string name="sensor_state_heart_rate_value">%1$d lpm</string>
|
||||||
<string name="sensor_state_power">Potencia</string>
|
<string name="sensor_state_power">Potencia</string>
|
||||||
<string name="sensor_state_power_value">%1$d W</string>
|
<string name="sensor_state_power_value">%1$d W</string>
|
||||||
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido.</string>
|
<string name="settings_not_while_recording">No está disponible durante la grabación de un recorrido</string>
|
||||||
<string name="settings_show">Mostrar</string>
|
<string name="settings_show">Mostrar</string>
|
||||||
<string name="settings_advanced">Opciones avanzadas</string>
|
<string name="settings_advanced">Opciones avanzadas</string>
|
||||||
<string name="settings_chart_by_distance">Por distancia</string>
|
<string name="settings_chart_by_distance">Por distancia</string>
|
||||||
@@ -216,23 +216,23 @@
|
|||||||
<string name="settings_recording_auto_resume_track_timeout_always_summary">Siempre reanudar la grabación después de reiniciar</string>
|
<string name="settings_recording_auto_resume_track_timeout_always_summary">Siempre reanudar la grabación después de reiniciar</string>
|
||||||
<string name="settings_recording_auto_resume_track_timeout_never_summary">Nunca reanudar la grabación después de reiniciar</string>
|
<string name="settings_recording_auto_resume_track_timeout_never_summary">Nunca reanudar la grabación después de reiniciar</string>
|
||||||
<string name="settings_recording_auto_resume_track_timeout_summary">Reanudar una grabación al reiniciar el sistema si la última ubicación se grabó en los últimos %1$s</string>
|
<string name="settings_recording_auto_resume_track_timeout_summary">Reanudar una grabación al reiniciar el sistema si la última ubicación se grabó en los últimos %1$s</string>
|
||||||
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación autom.</string>
|
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación automática</string>
|
||||||
<string name="settings_recording_default_activity_title">Actividad predet.</string>
|
<string name="settings_recording_default_activity_title">Tipo de actividad predeterminada</string>
|
||||||
<string name="settings_recording_location_frequency_summary">%1$s entre las ubicaciones grabadas</string>
|
<string name="settings_recording_location_frequency_summary">%1$s entre las ubicaciones grabadas</string>
|
||||||
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s.</string>
|
<string name="settings_recording_max_recording_distance_summary">Se crea un nuevo tramo si la distancia entre dos ubicaciones es superior a %1$s</string>
|
||||||
<string name="settings_recording_max_recording_distance_title">Distancia máxima</string>
|
<string name="settings_recording_max_recording_distance_title">Distancia máxima</string>
|
||||||
<string name="settings_recording_min_recording_distance_title">Intervalo de distancia</string>
|
<string name="settings_recording_min_recording_distance_title">Intervalo de distancia</string>
|
||||||
<string name="settings_recording_min_recording_interval_title">Intervalo de tiempo</string>
|
<string name="settings_recording_min_recording_interval_title">Intervalo de tiempo</string>
|
||||||
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s.</string>
|
<string name="settings_recording_min_required_accuracy_summary">La ubicación se ignora si su precisión es inferior a %1$s</string>
|
||||||
<string name="settings_recording_min_required_accuracy_title">Precisión del GPS</string>
|
<string name="settings_recording_min_required_accuracy_title">Precisión del GPS</string>
|
||||||
<string name="settings_recording_track_name_date_iso_8601_option">Fecha (ISO 8601)</string>
|
<string name="settings_recording_track_name_date_iso_8601_option">Fecha (ISO 8601)</string>
|
||||||
<string name="settings_recording_track_name_date_local_option">Fecha (local)</string>
|
<string name="settings_recording_track_name_date_local_option">Fecha (local)</string>
|
||||||
<string name="settings_recording_track_name_number_option">Número</string>
|
<string name="settings_recording_track_name_number_option">Número</string>
|
||||||
<string name="settings_recording_track_name_title">Nombre de rec. predet.</string>
|
<string name="settings_recording_track_name_title">Nombre de recorrido predeterminado</string>
|
||||||
<string name="settings_reset">Restablecer</string>
|
<string name="settings_reset">Restablecer</string>
|
||||||
<string name="settings_reset_confirm_message">Todos los parámetros de configuración se restablecerán a los valores predeterminados.</string>
|
<string name="settings_reset_confirm_message">Todos los parámetros de configuración se restablecerán a los valores predeterminados.</string>
|
||||||
<string name="settings_reset_confirm_title">¿Restablecer ahora?</string>
|
<string name="settings_reset_confirm_title">¿Restablecer ahora?</string>
|
||||||
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración.</string>
|
<string name="settings_reset_done">Se restablecieron los valores predeterminados de todas las opciones de configuración</string>
|
||||||
<string name="settings_sensor">Sensor</string>
|
<string name="settings_sensor">Sensor</string>
|
||||||
<string name="settings_sensor_bluetooth">Bluetooth</string>
|
<string name="settings_sensor_bluetooth">Bluetooth</string>
|
||||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||||
@@ -242,7 +242,9 @@
|
|||||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||||
<string name="settings_stats_units_title">Unidades preferidas</string>
|
<string name="settings_stats_units_title">Unidades preferidas</string>
|
||||||
<string name="share_track_share_file">Compartir el archivo del recorrido</string>
|
<string name="share_track_share_file">Compartir el archivo del recorrido</string>
|
||||||
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.\n\nPara ver este recorrido, usa Google Earth u otro software de mapas.\n\n%1$s</string>
|
<string name="share_track_share_file_body">Creo que este recorrido podría interesarte.
|
||||||
|
\n
|
||||||
|
\n%1$s</string>
|
||||||
<string name="share_track_subject">Me gustaría compartir un recorrido contigo</string>
|
<string name="share_track_subject">Me gustaría compartir un recorrido contigo</string>
|
||||||
<string name="stats_average_moving_pace">Ritmo de movimiento promedio</string>
|
<string name="stats_average_moving_pace">Ritmo de movimiento promedio</string>
|
||||||
<string name="stats_average_moving_speed">Velocidad de movimiento promedio</string>
|
<string name="stats_average_moving_speed">Velocidad de movimiento promedio</string>
|
||||||
@@ -255,7 +257,7 @@
|
|||||||
<string name="stats_latitude">Latitud</string>
|
<string name="stats_latitude">Latitud</string>
|
||||||
<string name="stats_longitude">Longitud</string>
|
<string name="stats_longitude">Longitud</string>
|
||||||
<string name="stats_max">Máx.</string>
|
<string name="stats_max">Máx.</string>
|
||||||
<string name="stats_max_speed">Velocidad máx.</string>
|
<string name="stats_max_speed">Velocidad máxima</string>
|
||||||
<string name="stats_min">Mín.</string>
|
<string name="stats_min">Mín.</string>
|
||||||
<string name="stats_moving_time">Tiempo en movimiento</string>
|
<string name="stats_moving_time">Tiempo en movimiento</string>
|
||||||
<string name="stats_pace">Ritmo</string>
|
<string name="stats_pace">Ritmo</string>
|
||||||
@@ -275,7 +277,7 @@
|
|||||||
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo)</string>
|
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo)</string>
|
||||||
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
|
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
|
||||||
<string name="track_edit_new_track_title">Nuevo recorrido</string>
|
<string name="track_edit_new_track_title">Nuevo recorrido</string>
|
||||||
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura.</string>
|
<string name="track_list_empty_message">Comienza a grabar aquí tu próxima aventura</string>
|
||||||
<string name="tracks_few">%1$d recorridos</string>
|
<string name="tracks_few">%1$d recorridos</string>
|
||||||
<string name="tracks_many">%1$d recorridos</string>
|
<string name="tracks_many">%1$d recorridos</string>
|
||||||
<string name="tracks_one">1 recorrido</string>
|
<string name="tracks_one">1 recorrido</string>
|
||||||
@@ -309,7 +311,7 @@
|
|||||||
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
|
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
|
||||||
<string name="value_integer_mile">%1$d millas</string>
|
<string name="value_integer_mile">%1$d millas</string>
|
||||||
<string name="value_integer_mile_hour">%1$d mi/h</string>
|
<string name="value_integer_mile_hour">%1$d mi/h</string>
|
||||||
<string name="value_integer_minute">%1$d min.</string>
|
<string name="value_integer_minute">%1$d mín</string>
|
||||||
<string name="value_integer_percent">%1$d %%</string>
|
<string name="value_integer_percent">%1$d %%</string>
|
||||||
<string name="value_integer_second">%1$d s</string>
|
<string name="value_integer_second">%1$d s</string>
|
||||||
<string name="value_none">Ninguno</string>
|
<string name="value_none">Ninguno</string>
|
||||||
@@ -416,4 +418,145 @@
|
|||||||
<!-- Share image -->
|
<!-- Share image -->
|
||||||
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
|
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
|
||||||
<string name="share_image_body">Creo que esta imagen podría interesarte.</string>
|
<string name="share_image_body">Creo que esta imagen podría interesarte.</string>
|
||||||
|
<string name="about_preference_title">Acerca de OpenTracks</string>
|
||||||
|
<string name="about_description"><i> OpenTracks </i> es una aplicación de seguimiento deportivo que respeta completamente su privacidad.</string>
|
||||||
|
<string name="about_url">Página web del proyecto: <a href="%1$s">%1$s</a></string>
|
||||||
|
<string name="about_version_name">Nombre de la versión: %1$s</string>
|
||||||
|
<string name="bluetooth_disabled">Por favor habilite Bluetooth.</string>
|
||||||
|
<string name="settings_default_trackfileformat">Exportar/compartir formato de archivo</string>
|
||||||
|
<string name="settings_recording_show_on_lockscreen_while_recording_title">Mostrar estadísticas en pantalla de bloqueo</string>
|
||||||
|
<string name="permission_gps_failed">OpenTracks necesita permiso para usar el GPS.</string>
|
||||||
|
<string name="settings_recording_keep_screen_on_while_recording_title">Mantener la pantalla encendida</string>
|
||||||
|
<string name="settings_sensor_disconnected">Desconectado de %1$s</string>
|
||||||
|
<string name="instant_export_enabled_title">Exportación instantánea post-entrenamiento</string>
|
||||||
|
<string name="hold_to_stop">Mantener para parar</string>
|
||||||
|
<string name="hold_to_pause">Mantener para pausar</string>
|
||||||
|
<string name="settings_sensor_wheel_circumference">Circunferencia de la rueda (mm)</string>
|
||||||
|
<string name="sensor_state_cadence_avg">Ritmo promedio</string>
|
||||||
|
<string name="sensor_state_heart_rate_avg">Tasa cardíaca promedio</string>
|
||||||
|
<string name="sensor_state_heart_rate_max">Tasa cardíaca máxima</string>
|
||||||
|
<string name="sensor_state_cadence_max">Ritmo máximo</string>
|
||||||
|
<string name="stats_intervals">Intervalos</string>
|
||||||
|
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Mientras graba, mostrar estadísticas sin desbloquear el dispositivo.</string>
|
||||||
|
<string name="settings_recording_keep_screen_on_while_recording_summary">Mientras graba, mantener la pantalla encendida.</string>
|
||||||
|
<string name="settings_recording_fullscreen_on_while_recording_title">Pantalla completa</string>
|
||||||
|
<string name="settings_recording_fullscreen_on_while_recording_summary">Mientras graba, presentar en pantalla completa.</string>
|
||||||
|
<string name="settings_night_mode_title">Tema de interfaz de usuario</string>
|
||||||
|
<string name="settings_night_mode_option_system">Sistema</string>
|
||||||
|
<string name="settings_night_mode_option_day">Día</string>
|
||||||
|
<string name="settings_night_mode_option_night">Noche</string>
|
||||||
|
<string name="settings_error_initial_values_restored">Lo sentimos, pero ha ocurrido un error. Todas las configuraciones se han revertido a los valores predeterminados.</string>
|
||||||
|
<string name="settings_sensor_connecting">Conectado a %1$s</string>
|
||||||
|
<string name="track_speed_notification">Velocidad: %1$s</string>
|
||||||
|
<string name="instant_export_enabled_summary">Exportar el recorrido al almacenamiento una vez finalizada la grabación</string>
|
||||||
|
<string name="toast_message_instant_track_was_exported">Último recorrido exportado</string>
|
||||||
|
<string name="description_altitude_loss">Pérdida de elevación: %1$d m (%2$d pies)</string>
|
||||||
|
<string name="export_progress_summary_new_msg">Nuevos recorridos exportados</string>
|
||||||
|
<string name="about_version_code">Código de versión: %1$d</string>
|
||||||
|
<string name="about_history">OpenTracks es una bifurcación de <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks de Google </i> </a> (descontinuado en 2016).</string>
|
||||||
|
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de sensor de frecuencia cardíaca, esto significa que solo se almacena <i> una </i> medición de frecuencia cardíaca por ubicación. Si está parado o está adentro, no se registrarán los datos del sensor (pero se mostrarán). </string>
|
||||||
|
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer:
|
||||||
|
\na) compruebe si el GPS está habilitado,
|
||||||
|
\nb) mejorar la recepción del GPS (por ejemplo, salir al aire libre),
|
||||||
|
\nc) verifique con la aplicación <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a>, y
|
||||||
|
\nd) desactivar el ahorro de energía.
|
||||||
|
\n
|
||||||
|
\nSi todo no ayuda, informe un error. </string>
|
||||||
|
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
|
||||||
|
<string name="value_float_mile_hour">%1$.2f mph</string>
|
||||||
|
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
|
||||||
|
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
|
||||||
|
<string name="help_support_content"> Usted puede:
|
||||||
|
\na) haga correr la voz sobre OpenTracks,
|
||||||
|
\nb) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> proponer funciones </a>,
|
||||||
|
\nc) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> informar errores </a>,
|
||||||
|
\nd) ayuda en el desarrollo de software,
|
||||||
|
\ne) comprar en <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
|
||||||
|
\nf) donar, y
|
||||||
|
\ng) haga lo que crea que ayuda a OpenTracks. </string>
|
||||||
|
<string name="export_error_post_workout">La exportación posterior al entrenamiento falló, consulte el directorio de exportación.</string>
|
||||||
|
<plurals name="generic_completed_with_errors">
|
||||||
|
<item quantity="one">Completado con un error</item>
|
||||||
|
<item quantity="other">Completado con %1$d errores</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="import_thread_interrupted">Hilo interrumpido</string>
|
||||||
|
<string name="import_no_kml_file_found">No se ha encontrado ningún archivo doc.kml en KMZ</string>
|
||||||
|
<string name="sensor_state_power_avg">Potencia promedio</string>
|
||||||
|
<string name="sensor_not_known">El sensor %1$s es desconocido. Compruebe la configuración.</string>
|
||||||
|
<string name="sensor_could_not_scan">No se pudieron buscar dispositivos Bluetooth (error %1$i).</string>
|
||||||
|
<string name="settings_prevent_reimport_tracks_title">Evitar reimportar los recorridos</string>
|
||||||
|
<string name="track_distance_notification">Distancia: %1$s</string>
|
||||||
|
<string name="aggregated_stats_empty_message">Grabe su primer recorrido para ver estadísticas agregadas</string>
|
||||||
|
<string name="settings_default_export_uri_title">Directorio de exportación de recorrido</string>
|
||||||
|
<string name="help_track_map_content"> Utilice el icono de mapa pequeño.
|
||||||
|
\na) (actualizaciones en vivo) Instale <a href="https://github.com/OpenTracksApp/OSMDashboard"> OSMDashboard </a>. Proporciona mapas en línea o fuera de línea a través de <a href="https://www.openstreetmap.org"> OpenStreetMap </a>.
|
||||||
|
\nEn realidad, OSMDashboard es una aplicación separada que solo puede acceder (temporalmente) a los datos de las pistas que se mostrarán.
|
||||||
|
\nb) A través de una aplicación de mapas normal (p. ej., <a href="http://osmand.net/"> OsmAnd </a> y <a href="https://maps.me/"> MAPS.ME </a>). Estas aplicaciones deben admitir el formato de archivo KMZ (solo ubicaciones y marcas de tiempo). De hecho, una aplicación debe admitir <i> ACTION_VIEW </i> con MIME <i> application / vnd.google-earth.kmz</i>. </string>
|
||||||
|
<string name="voice_speed_lap">Velocidad de vuelta de %1$s</string>
|
||||||
|
<string name="voice_pace_lap">Tiempo de vuelta de %1$s</string>
|
||||||
|
<string name="delete_service">Borrar servicio</string>
|
||||||
|
<string name="about_third_party_data">Datos de terceros</string>
|
||||||
|
<string name="about_libraries">Bibliotecas</string>
|
||||||
|
<string name="help_feature_title">OpenTracks tiene las siguientes características:</string>
|
||||||
|
<string name="help_feature_content">* crear pistas de GPS
|
||||||
|
\n* utilizar un sensor de frecuencia cardíaca (solo Bluetooth LE)
|
||||||
|
\n* anuncios de voz (por tiempo o distancia)
|
||||||
|
\n* crear marcadores (incluidas imágenes)
|
||||||
|
\n* funciona completamente sin conexión
|
||||||
|
\n* mostrar pistas en un mapa (ver más abajo)</string>
|
||||||
|
<string name="help_track_map_title">¿Cómo mostrar un camino en un mapa\?</string>
|
||||||
|
<string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string>
|
||||||
|
<string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string>
|
||||||
|
<string name="help_track_export_title">¿Cómo puedo exportar / compartir pistas grabadas\?</string>
|
||||||
|
<string name="help_track_export_content"> Las pistas se pueden exportar desde la configuración como KMZ, KML o GPX. KMZ / KML todos los datos; para GPX solo ubicaciones. Las pistas compartidas a través del <i> icono de compartir </i> se comparten como KMZ. </string>
|
||||||
|
<string name="help_data_google_play_services_title">¿OpenTracks requiere / usa el servicio Google Play\?</string>
|
||||||
|
<string name="help_recording_title">¿Cómo registra OpenTracks los datos\?</string>
|
||||||
|
<string name="help_recording_content">OpenTracks utiliza el sensor GPS del dispositivo para registrar la ubicación de forma continua. Si el dispositivo GPS informa datos inexactos (por ejemplo, ubicación, velocidad, elevación), OpenTracks no puede hacer nada al respecto.</string>
|
||||||
|
<string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores\?</string>
|
||||||
|
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
|
||||||
|
<string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string>
|
||||||
|
<string name="open_track_using_dashboard">Abrir usando el Tablero OpenTracks</string>
|
||||||
|
<string name="open_track_as_KMZ">Abrir como KMZ</string>
|
||||||
|
<string name="open_track_as_GPX">Abrir como GPX</string>
|
||||||
|
<string name="open_track_as_trackfileformat">Abrir como %1$s</string>
|
||||||
|
<string name="activity_type_escooter">moto</string>
|
||||||
|
<string name="app_not_installed_show_on_map">Instale una aplicación que admita la presentación de datos de mapas.</string>
|
||||||
|
<string name="description_speed_ms">Velocidad (m/s)</string>
|
||||||
|
<string name="description_default_speed_or_pace">Por tipo de actividad</string>
|
||||||
|
<string name="export_with_photos">con fotos</string>
|
||||||
|
<string name="export_without_photos">sin fotos</string>
|
||||||
|
<string name="export_track_already_exists_msg">La pista %1$s ya existe en el directorio de destino.</string>
|
||||||
|
<string name="export_all_do_it_for_all">hacerlo para todos los conflictos</string>
|
||||||
|
<string name="export_track_errors">Recorridos no exportados</string>
|
||||||
|
<string name="export_progress_summary_overwrite_msg">Recorridos sobreescritos</string>
|
||||||
|
<string name="export_progress_summary_skip_msg">Recorridos salteados</string>
|
||||||
|
<string name="export_progress_summary_errors_msg">Errores</string>
|
||||||
|
<string name="generic_open_track">Abrir recorrido</string>
|
||||||
|
<string name="generic_overwrite">Sobreescribir</string>
|
||||||
|
<string name="generic_skip">Saltear</string>
|
||||||
|
<string name="generic_separator_done_total">/</string>
|
||||||
|
<string name="generic_show_errors">Mostrar errores</string>
|
||||||
|
<string name="generic_completed">Completado</string>
|
||||||
|
<string name="generic_click_twice_cancel">Haga nuevamente clic en atrás para cancelar</string>
|
||||||
|
<string name="gps_disabled_msg">GPS deshabilitado</string>
|
||||||
|
<string name="gps_fixed_and_ready">GPS establecido y listo</string>
|
||||||
|
<string name="import_unsupported_format">Formato de archivo no soportado</string>
|
||||||
|
<string name="import_parser_error">Error de parseo: %1$s</string>
|
||||||
|
<string name="import_unable_to_import_file">No se puede importar el archivo: %1$s</string>
|
||||||
|
<string name="import_error_info">%1$s: %2$s</string>
|
||||||
|
<string name="import_error_list_dialog_title">Archivos no importados</string>
|
||||||
|
<string name="import_file_imported">Archivo %1$s importado</string>
|
||||||
|
<string name="import_prevent_reimport">Ha configurado evitar la reimportación del recorrido</string>
|
||||||
|
<string name="import_progress_summary_ok_msg">Archivos importados</string>
|
||||||
|
<string name="import_progress_summary_exists_msg">Ya existe</string>
|
||||||
|
<string name="import_progress_summary_errors_msg">Errores</string>
|
||||||
|
<string name="marker_add_photo_canceled">Cancelado, no se agregó la foto como marcador</string>
|
||||||
|
<string name="stats_loss">Pérdida</string>
|
||||||
|
<string name="stats_split_interval">Intervalo partido</string>
|
||||||
|
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
|
||||||
|
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||||
|
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||||
|
<string name="track_detail_intervals_tab">Intervalos</string>
|
||||||
|
<string name="track_detail_resume_success">Ahora reanudando el recorrido</string>
|
||||||
|
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
|
||||||
</resources>
|
</resources>
|
||||||