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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
target: [default]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
uses: reactivecircus/android-emulator-runner@v2.17.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<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.TestClassGradleConfigurationProducer" />
|
||||
<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>
|
||||
</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
|
||||
<div>
|
||||
<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">
|
||||
</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
|
||||
|
||||
_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).
|
||||
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).
|
||||
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
|
||||
|
||||
@@ -114,11 +123,11 @@ Rework of _OpenTracks_ included:
|
||||
* removing calorie estimation and activity estimation,
|
||||
* removing support for ANT+ and Classic Bluetooth,
|
||||
* 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.
|
||||
|
||||
Artwork, logos and user interface remained more or less unchanged.
|
||||
|
||||
More information about _Google My Tracks_:
|
||||
* [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks)
|
||||
* [Google code archive](https://code.google.com/archive/p/mytracks/)
|
||||
* [Google code archive](https://code.google.com/archive/p/mytracks/)
|
||||
@@ -50,4 +50,5 @@ Also the distance is not computed.
|
||||
* QUARQ Red DZero Powermeter
|
||||
* Tacx Satori Smart
|
||||
* 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()
|
||||
}
|
||||
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 {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4046
|
||||
versionName "v3.18.0"
|
||||
versionCode 4143
|
||||
versionName "v3.18.3"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -93,7 +93,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
|
||||
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.documentfile:documentfile:1.0.1'
|
||||
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:rules: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:
|
||||
* Sport tracking: track your sport and outdoor activities
|
||||
* Photos and markers: mark interesting locations while tracking
|
||||
* Supports Bluetooth LE sensors:
|
||||
* heart rate
|
||||
* cycling: speed and cadence
|
||||
* cycling: power meter
|
||||
* Export data: export tracks either as KML or GPX
|
||||
* No Internet access: Internet is not used
|
||||
* Altitude gain and loss: via barometric sensor
|
||||
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* Voice announcements
|
||||
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
|
||||
* Export data: export tracks either as KMZ (incl. photos), KML, or GPX
|
||||
* 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.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.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static org.hamcrest.Matchers.anything;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -39,13 +42,13 @@ public class EspressoUITest {
|
||||
public void record_pause_resume_stop() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerRecordButton.perform(waitFor(5000));
|
||||
@@ -72,13 +75,13 @@ public class EspressoUITest {
|
||||
public void record_move_through_tabs() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
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(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) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -25,4 +28,35 @@ public class TrackPointTest {
|
||||
|
||||
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.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -100,7 +101,7 @@ public class SensorDataCyclingTest {
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous, 2150);
|
||||
current.compute(previous, Distance.ofMM(2150));
|
||||
|
||||
// then
|
||||
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);
|
||||
|
||||
// when
|
||||
current.compute(previous, 2000);
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
|
||||
// then
|
||||
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 importTrackId;
|
||||
|
||||
private TrackImporter trackImporter;
|
||||
|
||||
@Before
|
||||
public void fileSetup() throws IOException {
|
||||
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
|
||||
tmpFileUri = Uri.fromFile(tmpFile);
|
||||
|
||||
trackImporter = new TrackImporter(context, contentProviderUtils, Distance.of(10), Distance.of(200), true);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -169,8 +173,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -209,8 +213,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -248,8 +252,8 @@ public class ExportImportTest {
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// 2. import
|
||||
KmzTrackImporter trackImporter = new KmzTrackImporter();
|
||||
importTrackId = trackImporter.importFile(context, tmpFileUri).get(0);
|
||||
KmzTrackImporter importer = new KmzTrackImporter(context, trackImporter);
|
||||
importTrackId = importer.importFile(tmpFileUri).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -288,8 +292,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
@@ -313,8 +317,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -362,8 +366,8 @@ public class ExportImportTest {
|
||||
|
||||
// 2. import
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
@@ -406,18 +410,18 @@ public class ExportImportTest {
|
||||
// TODO Not exported for GPX/KML
|
||||
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
|
||||
|
||||
assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
assertEquals("" + i, trackPoint.hasLocation(), importedTrackPoint.hasLocation());
|
||||
if (trackPoint.hasLocation()) {
|
||||
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
|
||||
assertEquals("" + i, trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
|
||||
}
|
||||
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
|
||||
assertEquals("" + i, trackPoint.hasSpeed(), importedTrackPoint.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()) {
|
||||
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)) {
|
||||
@@ -426,13 +430,13 @@ public class ExportImportTest {
|
||||
}
|
||||
|
||||
if (verifyHeartrate) {
|
||||
assertEquals("" + trackPoint, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
|
||||
}
|
||||
if (verifyCadence) {
|
||||
assertEquals("" + trackPoint, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getCyclingCadence_rpm(), importedTrackPoint.getCyclingCadence_rpm(), 0.01);
|
||||
}
|
||||
if (verifyPower) {
|
||||
assertEquals("" + trackPoint, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
assertEquals("" + i, trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
}
|
||||
if (verifyAltitudeGain) {
|
||||
assertEquals(trackPoint.getAltitudeGain(), importedTrackPoint.getAltitudeGain(), 0.01);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -15,6 +16,7 @@ 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;
|
||||
@@ -29,13 +31,20 @@ import static org.junit.Assert.assertNotNull;
|
||||
* Test that legacy KML/GPX formats can still be imported.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyImportTest {
|
||||
public class GPXImportTest {
|
||||
|
||||
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) {
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -107,12 +59,12 @@ public class LegacyImportTest {
|
||||
@Test
|
||||
public void gpx_with_pause_resume() throws IOException {
|
||||
// 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);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream).get(0);
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
// then
|
||||
// 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);
|
||||
}
|
||||
|
||||
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(type, trackPoint.getType());
|
||||
|
||||
@@ -154,6 +106,5 @@ public class LegacyImportTest {
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
@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")
|
||||
@Test
|
||||
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:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:hasFragileUserData="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ThemeCustom">
|
||||
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".TrackDeleteActivity"
|
||||
android:theme="@style/TrackDeleteActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
android:label="@string/menu_help" />
|
||||
@@ -350,6 +346,13 @@ limitations under the License.
|
||||
android:name=".io.file.importer.ImportService"
|
||||
android:exported="false"
|
||||
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>
|
||||
|
||||
<queries>
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -26,33 +24,38 @@ import java.util.Arrays;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
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.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* An abstract class for the following common tasks across
|
||||
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
||||
* <p>
|
||||
* - share track <br>
|
||||
* - delete tracks <br>
|
||||
*
|
||||
* @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
|
||||
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 static final int DELETE_REQUEST_CODE = 3;
|
||||
private TrackDeleteServiceConnection trackDeleteServiceConnection;
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DELETE_REQUEST_CODE) {
|
||||
onTrackDeleted();
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||
trackDeleteServiceConnection.bind(this);
|
||||
}
|
||||
|
||||
@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) {
|
||||
boolean stopRecording = false;
|
||||
|
||||
onDeleteConfirmed();
|
||||
|
||||
for (Track.Id trackId : trackIds) {
|
||||
if (trackId.equals(getRecordingTrackId())) {
|
||||
stopRecording = true;
|
||||
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
if (stopRecording) {
|
||||
getTrackRecordingServiceConnection().stopRecording(this);
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
|
||||
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
|
||||
startActivityForResult(intent, DELETE_REQUEST_CODE);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
protected Track.Id getRecordingTrackId() {
|
||||
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);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
@@ -261,15 +261,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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();
|
||||
|
||||
protected static final int GPS_REQUEST_CODE = 6;
|
||||
|
||||
// The following are set in onCreate
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackController trackController;
|
||||
private ResourceCursorAdapter resourceCursorAdapter;
|
||||
|
||||
private TrackListBinding viewBinding;
|
||||
@@ -118,7 +119,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
runOnUiThread(() -> {
|
||||
TrackListActivity.this.invalidateOptionsMenu();
|
||||
loaderCallbacks.restart();
|
||||
trackController.onResume(recordingStatus);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -132,9 +132,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// After binding changes (e.g., becomes available), update the total time in trackController.
|
||||
runOnUiThread(() -> trackController.update(recordingStatus));
|
||||
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
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);
|
||||
|
||||
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.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||
@@ -250,14 +239,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Update UI
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -265,7 +246,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
||||
trackController.onResume(recordingStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -298,7 +278,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
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);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
@@ -306,7 +292,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
updateMenuItems(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
|
||||
updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
|
||||
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setQuery("", false);
|
||||
@@ -355,6 +341,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -366,17 +358,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overridePendingTransition(int enterAnim, int exitAnim) {
|
||||
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@@ -396,20 +394,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
loaderCallbacks.setSearch(searchQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
// Hide trackController when search dialog is shown
|
||||
trackController.hide();
|
||||
return super.onSearchRequested();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleted() {
|
||||
protected void onDeleteConfirmed() {
|
||||
// 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.
|
||||
*
|
||||
* @param isGpsStarted true if gps is started
|
||||
* @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) {
|
||||
if (startGpsMenuItem != null) {
|
||||
startGpsMenuItem.setVisible(!isRecording);
|
||||
@@ -479,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (itemId == R.id.list_context_menu_edit) {
|
||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||
@@ -491,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
deleteTracks(trackIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.list_context_menu_select_all) {
|
||||
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
||||
viewBinding.trackList.setItemChecked(i, true);
|
||||
@@ -558,25 +540,25 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
public void recordStart() {
|
||||
if (recordingStatus.getTrackId() == null) {
|
||||
// Not recording -> Recording
|
||||
updateMenuItems(false, true);
|
||||
updateGpsMenuItem(false, true);
|
||||
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
|
||||
startActivity(newIntent);
|
||||
} else if (recordingStatus.isPaused()) {
|
||||
// Paused -> Resume
|
||||
updateMenuItems(false, true);
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordPause() {
|
||||
updateMenuItems(false, true);
|
||||
updateGpsMenuItem(false, true);
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordStop() {
|
||||
updateMenuItems(false, false);
|
||||
updateGpsMenuItem(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
|
||||
}
|
||||
|
||||
@@ -585,10 +567,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
updateGpsMenuItem(true, recordingStatus.isRecording());
|
||||
}
|
||||
|
||||
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
|
||||
trackController.update(recordingStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
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.provider.ContentProviderUtils;
|
||||
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.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
@@ -56,14 +56,13 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//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();
|
||||
|
||||
public static final String VIEW_TRACK_ICON = "track_icon";
|
||||
|
||||
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";
|
||||
|
||||
@@ -248,9 +247,16 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
return recordingStatus.getTrackId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
super.onTrackDeleteStatus(deleteStatus);
|
||||
if (deleteStatus.isDeleted(trackId)) {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleted() {
|
||||
protected void onDeleteConfirmed() {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@@ -264,18 +270,6 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
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) {
|
||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||
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.
|
||||
//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";
|
||||
|
||||
@@ -56,7 +56,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackDataHub trackDataHub;
|
||||
private TrackController trackController;
|
||||
|
||||
private TrackRecordingBinding viewBinding;
|
||||
|
||||
@@ -90,8 +89,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
// A recording track is on.
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
|
||||
trackController.onResume(recordingStatus);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -148,8 +145,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
if (savedInstanceState != null) {
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +209,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
//TODO Pass recordingStatus directly to them
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
trackController.onResume(recordingStatus);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -233,12 +227,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
@@ -406,7 +394,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
|
||||
recordingStatus = status;
|
||||
|
||||
trackController.update(recordingStatus);
|
||||
trackDataHub.setRecordingStatus(recordingStatus);
|
||||
|
||||
setLockscreenPolicy();
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
@@ -40,8 +41,8 @@ import androidx.core.view.GestureDetectorCompat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.dennisguse.opentracks.MarkerDetailActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -114,7 +115,7 @@ public class ChartView extends View {
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
private final boolean chartByDistance;
|
||||
private boolean chartByDistance = false;
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
@@ -189,9 +190,9 @@ public class ChartView extends View {
|
||||
}
|
||||
});
|
||||
|
||||
public ChartView(Context context, final boolean chartByDistance) {
|
||||
super(context);
|
||||
this.chartByDistance = chartByDistance;
|
||||
public ChartView(Context context, AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
|
||||
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
||||
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
||||
|
||||
@@ -366,6 +367,10 @@ public class ChartView extends View {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setChartByDistance(boolean chartByDistance) {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
public boolean getMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
@@ -424,7 +429,7 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
updateDimensions();
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,7 +476,7 @@ public class ChartView extends View {
|
||||
private void zoomIn() {
|
||||
if (canZoomIn()) {
|
||||
zoomLevel++;
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -486,7 +491,7 @@ public class ChartView extends View {
|
||||
scrollX = maxWidth;
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
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.
|
||||
*/
|
||||
private void updatePaths() {
|
||||
private void updateSeries() {
|
||||
synchronized (chartPoints) {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
chartValueSeries.getPath().reset();
|
||||
}
|
||||
drawPaths();
|
||||
closePaths();
|
||||
seriesList.stream().forEach(this::updateSerie);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPaths() {
|
||||
boolean[] hasMoved = new boolean[seriesList.size()];
|
||||
private void updateSerie(ChartValueSeries series) {
|
||||
final int yCorner = topBorder + effectiveHeight;
|
||||
final Path path = series.getPath();
|
||||
|
||||
for (ChartPoint dataPoint : chartPoints) {
|
||||
for (int i = 0; i < seriesList.size(); i++) {
|
||||
ChartValueSeries chartValueSeries = seriesList.get(i);
|
||||
boolean drawFirstPoint = false;
|
||||
path.rewind();
|
||||
|
||||
if (!chartValueSeries.isChartPointValid(dataPoint)) {
|
||||
continue;
|
||||
}
|
||||
Iterator<ChartPoint> iterator = chartPoints.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
ChartPoint point = iterator.next();
|
||||
if (!series.isChartPointValid(point)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
|
||||
Path path = chartValueSeries.getPath();
|
||||
int x = getX(dataPoint.getTimeOrDistance());
|
||||
int y = getY(chartValueSeries, value);
|
||||
if (!hasMoved[i]) {
|
||||
hasMoved[i] = true;
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
double value = series.extractDataFromChartPoint(point);
|
||||
int x = getX(point.getTimeOrDistance());
|
||||
int y = getY(series, value);
|
||||
|
||||
// start from lower left corner
|
||||
if (!drawFirstPoint) {
|
||||
path.moveTo(x, yCorner);
|
||||
drawFirstPoint = true;
|
||||
}
|
||||
|
||||
// draw graph
|
||||
path.lineTo(x, y);
|
||||
|
||||
// last point: move to lower right
|
||||
if (!iterator.hasNext()) {
|
||||
path.lineTo(x, yCorner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void closePaths() {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
// back to lower left corner
|
||||
path.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -961,7 +952,7 @@ public class ChartView extends View {
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
updateEffectiveDimensions();
|
||||
updatePaths();
|
||||
updateSeries();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Altitude;
|
||||
@@ -71,13 +72,13 @@ public class TrackDataHub {
|
||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final TrackDataManager trackDataManager;
|
||||
private final Set<TrackDataListener> listeners;
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final int targetNumPoints;
|
||||
|
||||
private EGM2008Utils.EGM2008Correction egm2008Correction;
|
||||
|
||||
private boolean started;
|
||||
//TODO Check if this is needed.
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
|
||||
@@ -99,24 +100,23 @@ public class TrackDataHub {
|
||||
private ContentObserver trackPointsTableObserver;
|
||||
|
||||
public TrackDataHub(Context context) {
|
||||
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
this(context, new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
private TrackDataHub(Context context, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
this.context = context;
|
||||
this.trackDataManager = trackDataManager;
|
||||
this.listeners = new HashSet<>();
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.targetNumPoints = targetNumPoints;
|
||||
resetSamplingState();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (started) {
|
||||
if (isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
@@ -126,7 +126,7 @@ public class TrackDataHub {
|
||||
tracksTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
|
||||
@@ -134,7 +134,7 @@ public class TrackDataHub {
|
||||
markersTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
};
|
||||
contentResolver.registerContentObserver(MarkerColumns.CONTENT_URI, false, markersTableObserver);
|
||||
@@ -142,21 +142,18 @@ public class TrackDataHub {
|
||||
trackPointsTableObserver = new ContentObserver(handler) {
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!started) {
|
||||
if (!isStarted()) {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = false;
|
||||
trackStatisticsUpdater = null;
|
||||
|
||||
//Unregister listeners
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
contentResolver.unregisterContentObserver(tracksTableObserver);
|
||||
@@ -168,6 +165,7 @@ public class TrackDataHub {
|
||||
handlerThread = null;
|
||||
}
|
||||
handler = null;
|
||||
trackStatisticsUpdater = null;
|
||||
}
|
||||
|
||||
public void loadTrack(final @NonNull Track.Id trackId) {
|
||||
@@ -186,10 +184,10 @@ public class TrackDataHub {
|
||||
*
|
||||
* @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(() -> {
|
||||
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, markersTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
|
||||
if (started) {
|
||||
listeners.add(trackDataListener);
|
||||
if (isStarted()) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
});
|
||||
@@ -201,7 +199,7 @@ public class TrackDataHub {
|
||||
* @param trackDataListener the track data listener
|
||||
*/
|
||||
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() {
|
||||
resetSamplingState();
|
||||
if (!trackDataManager.hasListeners()) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
|
||||
notifyTracksTableUpdate(listeners);
|
||||
|
||||
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
|
||||
notifyMarkersTableUpdate(trackDataManager.getListenerMarkers());
|
||||
notifyTrackPointsTableUpdate(true, listeners);
|
||||
notifyMarkersTableUpdate(listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,25 +242,19 @@ public class TrackDataHub {
|
||||
private void loadDataForListener(TrackDataListener trackDataListener) {
|
||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||
|
||||
if (trackDataManager.listensForTracks(trackDataListener)) {
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Track
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
|
||||
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
|
||||
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
|
||||
if (hasSampledIn || hasSampledOut) {
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.emptySet();
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
|
||||
//TrackPoints
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = listeners.size() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners);
|
||||
|
||||
if (trackDataManager.listensForMarkers(trackDataListener)) {
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
//Markers
|
||||
notifyMarkersTableUpdate(trackDataListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,11 +307,9 @@ public class TrackDataHub {
|
||||
* Notifies track points table update; to be run in the {@link #handler} thread.
|
||||
*
|
||||
* @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) {
|
||||
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
|
||||
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,7 +317,7 @@ public class TrackDataHub {
|
||||
// Reload and resample the track at a lower frequency.
|
||||
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
|
||||
resetSamplingState();
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
}
|
||||
@@ -355,6 +345,13 @@ public class TrackDataHub {
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
|
||||
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (!isStarted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Prevents a NPE if stop() is happening while notifyTrackPointsTableUpdate()
|
||||
TrackStatisticsUpdater currentUpdater = trackStatisticsUpdater;
|
||||
|
||||
trackPoint = trackPointIterator.next();
|
||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||
|
||||
@@ -374,16 +371,16 @@ public class TrackDataHub {
|
||||
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.
|
||||
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
|
||||
for (TrackDataListener trackDataListener : sampledInListeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics(), trackStatisticsUpdater.getSmoothedSpeed(), trackStatisticsUpdater.getSmoothedAltitude());
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledInTrackPoint(trackPoint, currentUpdater.getTrackStatistics(), currentUpdater.getSmoothedSpeed(), currentUpdater.getSmoothedAltitude());
|
||||
}
|
||||
} else {
|
||||
for (TrackDataListener trackDataListener : sampledOutListeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics());
|
||||
for (TrackDataListener trackDataListener : listeners) {
|
||||
trackDataListener.onSampledOutTrackPoint(trackPoint, currentUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,11 +398,7 @@ public class TrackDataHub {
|
||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||
}
|
||||
|
||||
if (trackPoint != null) {
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
listener.onNewTrackPointsDone(trackPoint, trackStatisticsUpdater.getTrackStatistics());
|
||||
}
|
||||
}
|
||||
listeners.stream().forEach(TrackDataListener::onNewTrackPointsDone);
|
||||
}
|
||||
|
||||
private void correctAltitude(TrackPoint trackPoint) {
|
||||
@@ -436,6 +429,10 @@ public class TrackDataHub {
|
||||
trackStatisticsUpdater = new TrackStatisticsUpdater();
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return handlerThread != null;
|
||||
}
|
||||
|
||||
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
|
||||
this.recordingStatus = recordingStatus;
|
||||
}
|
||||
|
||||
@@ -62,26 +62,28 @@ public interface TrackDataListener {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void clearMarkers();
|
||||
default void clearMarkers() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new marker is read.
|
||||
*
|
||||
* @param marker the marker
|
||||
*/
|
||||
void onNewMarker(Marker marker);
|
||||
default void onNewMarker(@NonNull Marker marker) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when finish sending new markers.
|
||||
* 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);
|
||||
}
|
||||
|
||||
public static Distance ofMM(double distance_mm) {
|
||||
return of(distance_mm * UnitConversions.MM_TO_M);
|
||||
}
|
||||
|
||||
public static Distance one(boolean metricUnit) {
|
||||
if (metricUnit) {
|
||||
return Distance.ofKilometer(1);
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class Marker {
|
||||
private String description = "";
|
||||
private String category = "";
|
||||
private String icon = "";
|
||||
private final Track.Id trackId;
|
||||
private Track.Id trackId;
|
||||
|
||||
private final Instant time;
|
||||
private Double latitude;
|
||||
@@ -74,16 +74,13 @@ public final class Marker {
|
||||
if (!trackPoint.hasLocation())
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
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();
|
||||
setTrackPoint(trackPoint);
|
||||
|
||||
this.length = Distance.of(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) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
@@ -95,6 +92,15 @@ public final class Marker {
|
||||
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.
|
||||
*/
|
||||
@@ -143,16 +149,19 @@ public final class Marker {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Track.Id getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setTrackId(@NonNull Track.Id trackId) {
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return latitude != null || longitude != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
location.setTime(time.toEpochMilli());
|
||||
|
||||
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO Do not default initialize attributes; might be confusing for debugging
|
||||
public class Track {
|
||||
|
||||
private Track.Id id;
|
||||
@@ -46,9 +47,6 @@ public class Track {
|
||||
|
||||
private TrackStatistics trackStatistics = new TrackStatistics();
|
||||
|
||||
public Track() {
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the track was not loaded from the database.
|
||||
*/
|
||||
|
||||
@@ -55,7 +55,7 @@ public class TrackPoint {
|
||||
private Altitude altitude;
|
||||
private Speed speed;
|
||||
private Float bearing;
|
||||
private Distance sensorDistance_m;
|
||||
private Distance sensorDistance;
|
||||
|
||||
public enum Type {
|
||||
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) {
|
||||
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.longitude = location.getLongitude();
|
||||
@@ -107,7 +111,9 @@ public class TrackPoint {
|
||||
this.speed = Speed.of(location.getSpeed());
|
||||
this.accuracy = location.getAccuracy();
|
||||
|
||||
setTime(Instant.now());
|
||||
//TODO Should we copy the bearing?
|
||||
|
||||
setTime(time);
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, Instant time) {
|
||||
@@ -198,6 +204,7 @@ public class TrackPoint {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
//TODO Better return null, if no location is present aka latitude == null etc.
|
||||
@NonNull
|
||||
public Location getLocation() {
|
||||
Location location = new Location("");
|
||||
@@ -314,10 +321,14 @@ public class TrackPoint {
|
||||
this.accuracy = horizontalAccuracy;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (hasSensorDistance()) {
|
||||
return getSensorDistance();
|
||||
}
|
||||
if (!(hasLocation() && previous.hasLocation())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Distance.of(getLocation().distanceTo(previous.getLocation()));
|
||||
}
|
||||
@@ -326,25 +337,27 @@ public class TrackPoint {
|
||||
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) {
|
||||
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) {
|
||||
return getLocation().bearingTo(dest);
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
public boolean hasSensorDistance() {
|
||||
return sensorDistance_m != null;
|
||||
return sensorDistance != null;
|
||||
}
|
||||
|
||||
public Distance getSensorDistance() {
|
||||
return sensorDistance_m;
|
||||
return sensorDistance;
|
||||
}
|
||||
|
||||
public void setSensorDistance(Distance distance_m) {
|
||||
this.sensorDistance_m = distance_m;
|
||||
this.sensorDistance = distance_m;
|
||||
}
|
||||
|
||||
public boolean hasSensorData() {
|
||||
|
||||
@@ -61,7 +61,6 @@ import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
public class ContentProviderUtils {
|
||||
|
||||
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.
|
||||
@VisibleForTesting
|
||||
@@ -161,8 +160,10 @@ public class ContentProviderUtils {
|
||||
|
||||
@VisibleForTesting
|
||||
public void deleteAllTracks(Context context) {
|
||||
//TODO Both calls should not be necessary
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
|
||||
contentResolver.delete(MarkerColumns.CONTENT_URI, null, null);
|
||||
|
||||
// Delete tracks last since it triggers a database vaccum call
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
|
||||
@@ -190,6 +191,7 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
//TODO Only use for tests; also move to tests.
|
||||
@VisibleForTesting
|
||||
public List<Track> getTracks() {
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
|
||||
@@ -203,13 +205,7 @@ public class ContentProviderUtils {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param trackId the track id.
|
||||
*/
|
||||
public Track getTrack(Track.Id trackId) {
|
||||
if (trackId == null) {
|
||||
return null;
|
||||
}
|
||||
public Track getTrack(@NonNull Track.Id trackId) {
|
||||
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return createTrack(cursor);
|
||||
@@ -218,9 +214,6 @@ public class ContentProviderUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param trackUUID the track uuid.
|
||||
*/
|
||||
public Track getTrack(@NonNull UUID trackUUID) {
|
||||
String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
|
||||
try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) {
|
||||
@@ -250,6 +243,7 @@ public class ContentProviderUtils {
|
||||
* @param track the track
|
||||
* @return the content provider URI of the inserted track.
|
||||
*/
|
||||
///TODO Return Track.Id
|
||||
public Uri insertTrack(Track 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 Rename to bulkInsert
|
||||
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
|
||||
ContentValues[] values = new ContentValues[trackPoints.size()];
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
@@ -584,6 +579,15 @@ public class ContentProviderUtils {
|
||||
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.
|
||||
* Returns -1L if it doesn't exist.
|
||||
@@ -609,6 +613,7 @@ public class ContentProviderUtils {
|
||||
* @param location the location
|
||||
* @return trackPoint id if the location is in the track. -1L otherwise.
|
||||
*/
|
||||
@Deprecated
|
||||
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
|
||||
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
|
||||
@@ -740,6 +745,7 @@ public class ContentProviderUtils {
|
||||
return new TrackPointIterator(this, trackId, startTrackPointId);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
|
||||
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
|
||||
@@ -123,7 +123,7 @@ public final class SensorDataCycling {
|
||||
return wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
|
||||
public void compute(DistanceSpeed previous, Distance wheelCircumference) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_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);
|
||||
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;
|
||||
if (previous.hasValue()) {
|
||||
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
||||
|
||||
@@ -16,28 +16,25 @@
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
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.Track;
|
||||
@@ -68,7 +65,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private Distance recordingDistanceInterval;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
@@ -80,40 +76,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
private boolean chartByDistance;
|
||||
|
||||
private ChartBinding viewBinding;
|
||||
// UI elements
|
||||
private ChartView chartView;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
if (metricUnits != chartView.getMetricUnits()) {
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
|
||||
viewBinding.chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
viewBinding.chartView.requestLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
|
||||
if (reportSpeed != chartView.getReportSpeed()) {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
chartView.applyReportSpeed();
|
||||
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||
viewBinding.chartView.applyReportSpeed();
|
||||
|
||||
runOnUiThread(() -> {
|
||||
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() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || trackDataHub == null) {
|
||||
if (!isResumed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
chartView.setShowPointer(isSelectedTrackRecording());
|
||||
chartView.invalidate();
|
||||
viewBinding.chartView.setShowPointer(isSelectedTrackRecording());
|
||||
viewBinding.chartView.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,27 +128,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
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
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = ChartBinding.inflate(inflater, container, false);
|
||||
viewBinding.chartView.setChartByDistance(chartByDistance);
|
||||
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
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -178,13 +156,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
viewBinding.chartViewLayout.removeView(chartView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -200,16 +171,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
if (track == null || track.getTrackStatistics() == null) {
|
||||
if (track == null) {
|
||||
category = "";
|
||||
return;
|
||||
}
|
||||
|
||||
category = track.getCategory();
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category);
|
||||
if (reportSpeed != chartView.getReportSpeed()) {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
chartView.applyReportSpeed();
|
||||
if (reportSpeed != viewBinding.chartView.getReportSpeed()) {
|
||||
viewBinding.chartView.setReportSpeed(reportSpeed);
|
||||
viewBinding.chartView.applyReportSpeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,10 +189,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
pendingPoints.clear();
|
||||
chartView.reset();
|
||||
viewBinding.chartView.reset();
|
||||
runOnUiThread(() -> {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
chartView.addChartPoints(pendingPoints);
|
||||
//Avoid ConcurrentModificationException exception
|
||||
viewBinding.chartView.addChartPoints(Collections.unmodifiableList(pendingPoints));
|
||||
pendingPoints.clear();
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
@@ -246,14 +218,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void clearMarkers() {
|
||||
if (isResumed()) {
|
||||
chartView.clearMarker();
|
||||
viewBinding.chartView.clearMarker();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewMarker(Marker marker) {
|
||||
if (isResumed() && marker != null) {
|
||||
chartView.addMarker(marker);
|
||||
public void onNewMarker(@NonNull Marker marker) {
|
||||
if (isResumed()) {
|
||||
viewBinding.chartView.addMarker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,9 +240,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
* Checks the chart settings.
|
||||
*/
|
||||
private void checkChartSettings() {
|
||||
boolean needUpdate = chartView.applyReportSpeed();
|
||||
boolean needUpdate = viewBinding.chartView.applyReportSpeed();
|
||||
if (needUpdate) {
|
||||
chartView.postInvalidate();
|
||||
viewBinding.chartView.postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +252,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
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
|
||||
*/
|
||||
private void runOnUiThread(Runnable runnable) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
Activity fragmentActivity = getActivity();
|
||||
if (fragmentActivity != null) {
|
||||
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.content.TrackDataHub;
|
||||
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.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -195,7 +194,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
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
|
||||
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -22,10 +21,6 @@ import java.util.List;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
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.Track;
|
||||
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.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
@@ -46,25 +40,17 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
* @author Sandor Dornbush
|
||||
* @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 Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
||||
|
||||
public static Fragment newInstance() {
|
||||
return new StatisticsRecordingFragment();
|
||||
}
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
private Handler handlerUpdateUI;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||
private TrackPoint lastTrackPoint;
|
||||
|
||||
@Nullable // Lazily loaded.
|
||||
private Track track;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
|
||||
private TrackPoint latestTrackPoint;
|
||||
|
||||
private StatisticsRecordingBinding viewBinding;
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
@@ -83,9 +69,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
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;
|
||||
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)) {
|
||||
@@ -99,39 +85,29 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
if (key != null && updateUInecessary && isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
getActivity().runOnUiThread(this::updateUI);
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable updateUIeachSecond = new Runnable() {
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed() && isSelectedTrackRecording()) {
|
||||
if (!isSelectedTrackPaused() && track != null) {
|
||||
synchronized (this) {
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation() && !lastTrackPoint.isRecent()) {
|
||||
lastTrackPoint = null;
|
||||
setLocationValues();
|
||||
}
|
||||
}
|
||||
updateTotalTime();
|
||||
updateSensorDataUI();
|
||||
}
|
||||
|
||||
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
|
||||
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.w(TAG, "could not get TrackRecordingService");
|
||||
return;
|
||||
}
|
||||
|
||||
service.getRecordingDataObservable()
|
||||
.observe(StatisticsRecordingFragment.this, recordingData -> onRecordingDataChanged(recordingData));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handlerUpdateUI = new Handler();
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
}
|
||||
@@ -139,7 +115,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
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;
|
||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||
@@ -151,25 +127,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
|
||||
updateUIeachSecond.run();
|
||||
handlerUpdateUI.post(updateUIeachSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,101 +160,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
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() {
|
||||
updateStats();
|
||||
setLocationValues();
|
||||
updateSensorDataUI();
|
||||
if (isResumed()) {
|
||||
updateStats();
|
||||
setLocationValues();
|
||||
updateSensorDataUI();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,100 +173,45 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
* Also sets altitude gain and loss.
|
||||
*/
|
||||
private void updateSensorDataUI() {
|
||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
|
||||
if (trackRecordingService == null) {
|
||||
Log.d(TAG, "Cannot get the track recording service.");
|
||||
} else {
|
||||
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);
|
||||
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorDataSet.getHeartRate() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
||||
}
|
||||
//TODO Check if we can get the total altitude gain and loss via trackStatistics instead of doing some computation in the UI layer.
|
||||
setTotalAltitudeGain(trackRecordingService.getAltitudeGain_m());
|
||||
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.getCyclingCadence() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
||||
}
|
||||
|
||||
totalAltitudeGain += track.getTrackStatistics().getTotalAltitudeGain();
|
||||
if (sensorDataSet.getCyclingPower() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
||||
}
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeGain, preferenceMetricUnits);
|
||||
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(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeGainValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeGainUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), totalAltitudeLoss, preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||
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);
|
||||
}
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStats() {
|
||||
|
||||
setTotalDistance(Distance.of(0)); //TODO Why?
|
||||
|
||||
// Set activity type
|
||||
if (track != null) {
|
||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), track.getCategory());
|
||||
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
|
||||
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
||||
}
|
||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
|
||||
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
||||
|
||||
// Set time
|
||||
if (track != null) {
|
||||
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
|
||||
updateTotalTime();
|
||||
}
|
||||
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
|
||||
updateTotalTime();
|
||||
|
||||
// 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);
|
||||
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -407,7 +232,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -417,34 +242,41 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
}
|
||||
|
||||
// Set altitude gain and loss
|
||||
{
|
||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateTotalTime() {
|
||||
Duration totalTime = track.getTrackStatistics().getTotalTime();
|
||||
if (isSelectedTrackRecording()) {
|
||||
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
totalTime = trackRecordingService.getTotalTime();
|
||||
}
|
||||
}
|
||||
|
||||
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
|
||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||
}
|
||||
|
||||
private void setLocationValues() {
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (preferenceShowAltitude) {
|
||||
// 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);
|
||||
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
|
||||
@@ -455,23 +287,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
||||
if (preferenceShowCoordinate) {
|
||||
String latitudeText = getContext().getString(R.string.value_unknown);
|
||||
String longitudeText = getContext().getString(R.string.value_unknown);
|
||||
if (lastTrackPoint != null && lastTrackPoint.hasLocation()) {
|
||||
latitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLatitude());
|
||||
longitudeText = StringUtils.formatCoordinate(lastTrackPoint.getLongitude());
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
|
||||
latitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLatitude());
|
||||
longitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLongitude());
|
||||
}
|
||||
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||
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) {
|
||||
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.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 {
|
||||
|
||||
@Deprecated //Not needed anymore; we now have OSMDashboard; was only used to share with other applications
|
||||
KML_ONLY_TRACK {
|
||||
@Override
|
||||
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:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
|
||||
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:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
|
||||
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
|
||||
|
||||
@@ -518,13 +518,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
return location.getBearing();
|
||||
}
|
||||
|
||||
private String getCoordinates(Location location, String separator) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(location.getLongitude()).append(separator).append(location.getLatitude());
|
||||
private static String getCoordinates(Location location, String separator) {
|
||||
String result = location.getLongitude() + separator + location.getLatitude();
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
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.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Imports a GPX file.
|
||||
@@ -43,7 +49,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
*
|
||||
* @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_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_DISTANCE = "opentracks:distance";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public GpxFileTrackImporter(Context context) {
|
||||
this(context, new ContentProviderUtils(context));
|
||||
}
|
||||
private Locator locator;
|
||||
|
||||
@VisibleForTesting
|
||||
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
||||
super(context, contentProviderUtils);
|
||||
private final Context context;
|
||||
|
||||
// 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
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,7 +127,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerStart(attributes);
|
||||
break;
|
||||
case TAG_TRACK:
|
||||
onTrackStart();
|
||||
trackImporter.newTrack();
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
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
|
||||
public void endElement(String uri, String localName, String tag) {
|
||||
switch (tag) {
|
||||
@@ -117,13 +153,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerEnd();
|
||||
break;
|
||||
case TAG_TRACK:
|
||||
onTrackEnd();
|
||||
trackImporter.setTrack(context, name, uuid, description, category, null);
|
||||
break;
|
||||
case TAG_TRACK_SEGMENT:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_TRACK_POINT:
|
||||
onTrackPointEnd();
|
||||
currentSegment.add(createTrackPoint());
|
||||
break;
|
||||
case TAG_NAME:
|
||||
if (content != null) {
|
||||
@@ -197,33 +233,65 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
content = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackStart() throws SAXException {
|
||||
super.onTrackStart();
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
private void onTrackSegmentStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackPoint createTrackPoint() throws ParsingException {
|
||||
TrackPoint trackPoint = super.createTrackPoint();
|
||||
private void onTrackSegmentEnd() {
|
||||
if (currentSegment.isEmpty()) {
|
||||
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) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (heartrate != null) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -239,7 +307,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (power != null) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -247,21 +315,21 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (gain != null) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (loss != null) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (distance != null) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -269,11 +337,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point start.
|
||||
*
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
private void onTrackPointStart(Attributes attributes) {
|
||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||
@@ -285,15 +348,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
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) {
|
||||
name = null;
|
||||
description = null;
|
||||
@@ -307,6 +361,57 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
|
||||
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.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
@@ -16,9 +17,12 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
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.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
public class ImportService extends JobIntentService {
|
||||
|
||||
@@ -50,13 +54,19 @@ public class ImportService extends JobIntentService {
|
||||
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
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)) {
|
||||
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)) {
|
||||
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)) {
|
||||
trackIds.addAll(new KmzTrackImporter().importFile(this, file.getUri()));
|
||||
trackIds.addAll(new KmzTrackImporter(this, trackImporter).importFile(file.getUri()));
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.time.Instant;
|
||||
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.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.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
*
|
||||
* @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;
|
||||
|
||||
@@ -64,29 +69,49 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
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 final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private final ArrayList<Float> speedList = new ArrayList<>();
|
||||
private final ArrayList<Float> distanceList = new ArrayList<>();
|
||||
private final ArrayList<Float> cadenceList = new ArrayList<>();
|
||||
private final ArrayList<Float> heartRateList = new ArrayList<>();
|
||||
private final ArrayList<Float> powerList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
|
||||
|
||||
public KmlFileTrackImporter(Context context) {
|
||||
this(context, new ContentProviderUtils(context));
|
||||
}
|
||||
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
@VisibleForTesting
|
||||
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
|
||||
super(context, contentProviderUtils);
|
||||
// The current element content
|
||||
private String content = "";
|
||||
|
||||
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
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,11 +123,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerStart();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
trackStarted = true;
|
||||
onTrackStart();
|
||||
trackImporter.newTrack();
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
if (!trackStarted) {
|
||||
if (trackImporter == null) {
|
||||
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
||||
}
|
||||
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
|
||||
public void endElement(String uri, String localName, String tag) throws SAXException {
|
||||
switch (tag) {
|
||||
@@ -128,13 +157,13 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
onMarkerLocationEnd();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
onTrackEnd();
|
||||
trackImporter.setTrack(context, name, uuid, description, category, icon);
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_GX_COORD:
|
||||
onTrackPointEnd();
|
||||
onCoordEnded();
|
||||
break;
|
||||
case TAG_GX_VALUE:
|
||||
onExtendedDataValueEnd();
|
||||
@@ -166,7 +195,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
break;
|
||||
case TAG_WHEN:
|
||||
if (content != null) {
|
||||
time = content.trim();
|
||||
whenList.add(StringUtils.parseTime(content.trim()));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -183,7 +212,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
content = "";
|
||||
}
|
||||
|
||||
private void onMarkerStart() {
|
||||
@@ -196,7 +225,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
latitude = null;
|
||||
longitude = null;
|
||||
altitude = null;
|
||||
time = null;
|
||||
markerType = null;
|
||||
}
|
||||
|
||||
@@ -204,7 +232,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (!MARKER_STYLE.equals(markerType)) {
|
||||
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() {
|
||||
@@ -219,39 +270,56 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackSegmentStart() {
|
||||
super.onTrackSegmentStart();
|
||||
trackPoints.clear();
|
||||
speedList.clear();
|
||||
distanceList.clear();
|
||||
heartRateList.clear();
|
||||
cadenceList.clear();
|
||||
powerList.clear();
|
||||
private void onTrackSegmentStart() {
|
||||
locationList.clear();
|
||||
whenList.clear();
|
||||
|
||||
sensorSpeedList.clear();
|
||||
sensorDistanceList.clear();
|
||||
sensorHeartRateList.clear();
|
||||
sensorCadenceList.clear();
|
||||
sensorPowerList.clear();
|
||||
altitudeGainList.clear();
|
||||
altitudeLossList.clear();
|
||||
}
|
||||
|
||||
protected void onTrackSegmentEnd() {
|
||||
super.onTrackSegmentEnd();
|
||||
// Close a track segment by inserting the segment locations
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
private void onTrackSegmentEnd() {
|
||||
if (locationList.size() != whenList.size()) {
|
||||
throw new ImportParserException("<coords> and <when> should have the same count.");
|
||||
}
|
||||
|
||||
if (i < speedList.size() && speedList.get(i) != null) {
|
||||
trackPoint.setSpeed(Speed.of(speedList.get(i)));
|
||||
// Close a track segment by inserting the segment locations
|
||||
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()) {
|
||||
trackPoint.setHeartRate_bpm(heartRateList.get(i));
|
||||
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
|
||||
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
|
||||
}
|
||||
if (i < cadenceList.size()) {
|
||||
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
|
||||
if (i < sensorHeartRateList.size()) {
|
||||
trackPoint.setHeartRate_bpm(sensorHeartRateList.get(i));
|
||||
}
|
||||
if (i < powerList.size()) {
|
||||
trackPoint.setPower(powerList.get(i));
|
||||
if (i < sensorCadenceList.size()) {
|
||||
trackPoint.setCyclingCadence_rpm(sensorCadenceList.get(i));
|
||||
}
|
||||
if (i < sensorPowerList.size()) {
|
||||
trackPoint.setPower(sensorPowerList.get(i));
|
||||
}
|
||||
if (i < altitudeGainList.size()) {
|
||||
trackPoint.setAltitudeGain(altitudeGainList.get(i));
|
||||
@@ -260,18 +328,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
|
||||
}
|
||||
|
||||
insertTrackPoint(trackPoint);
|
||||
trackImporter.addTrackPoint(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point end. gx:coord end tag.
|
||||
*/
|
||||
private void onTrackPointEnd() {
|
||||
// Add trackPoint to trackPoints
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
private void onCoordEnded() {
|
||||
String[] parts = content.trim().split(" ");
|
||||
if (parts.length == 2 || parts.length == 3) {
|
||||
longitude = parts[0];
|
||||
@@ -279,33 +340,41 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
}
|
||||
|
||||
// Similar to GPX
|
||||
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
|
||||
TrackPoint trackPoint = getTrackPoint();
|
||||
if (isFirstTrackPointInSegment) {
|
||||
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
|
||||
locationList.add(createLocation(longitude, latitude, altitude));
|
||||
|
||||
trackPoint.setType(type);
|
||||
}
|
||||
trackPoints.add(trackPoint);
|
||||
|
||||
// Reset variables for next trackpoint (which might not have such data).
|
||||
time = null;
|
||||
longitude = null;
|
||||
latitude = null;
|
||||
altitude = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On extended data start. gx:SimpleArrayData start tag.
|
||||
*/
|
||||
private Location createLocation(String longitude, String latitude, String altitude) {
|
||||
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) {
|
||||
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* On extended data value end. gx:value end tag.
|
||||
*/
|
||||
private void onExtendedDataValueEnd() throws SAXException {
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
@@ -320,19 +389,19 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
}
|
||||
switch (extendedDataType) {
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
||||
speedList.add(value);
|
||||
sensorSpeedList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
|
||||
distanceList.add(value);
|
||||
sensorDistanceList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
|
||||
powerList.add(value);
|
||||
sensorPowerList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEART_RATE:
|
||||
heartRateList.add(value);
|
||||
sensorHeartRateList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE:
|
||||
cadenceList.add(value);
|
||||
sensorCadenceList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN:
|
||||
altitudeGainList.add(value);
|
||||
@@ -344,4 +413,28 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
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 Context context;
|
||||
private final Context context;
|
||||
private final TrackImporter trackImporter;
|
||||
|
||||
public KmzTrackImporter(Context context, TrackImporter trackImporter) {
|
||||
this.context = context;
|
||||
this.trackImporter = trackImporter;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
|
||||
this.context = context;
|
||||
public List<Track.Id> importFile(Uri fileUri) throws IOException {
|
||||
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
|
||||
|
||||
List<Track.Id> trackIdsWithImages = new ArrayList<>();
|
||||
@@ -205,7 +210,7 @@ public class KmzTrackImporter {
|
||||
}
|
||||
|
||||
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) {
|
||||
@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 {
|
||||
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
|
||||
return parser.getImportTrackIds();
|
||||
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
|
||||
} catch (SAXException | ParserConfigurationException | ParsingException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
if (parser.getImportTrackIds().size() > 0) {
|
||||
parser.cleanImport();
|
||||
@@ -58,6 +58,7 @@ public class XMLImporter {
|
||||
}
|
||||
|
||||
interface TrackParser {
|
||||
@Deprecated
|
||||
DefaultHandler getHandler();
|
||||
|
||||
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 Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
||||
|
||||
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;
|
||||
|
||||
// The following variables are set in onCreate:
|
||||
@@ -84,6 +87,15 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
private PeriodicTaskExecutor voiceExecutor;
|
||||
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 Distance recordingDistanceInterval;
|
||||
@@ -127,14 +139,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
private RecordingStatus recordingStatus;
|
||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
handler = new Handler();
|
||||
|
||||
recordingStatusObservable = new MutableLiveData<>();
|
||||
updateRecordingStatus(STATUS_DEFAULT);
|
||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
|
||||
|
||||
handlerServer = new HandlerServer(this);
|
||||
|
||||
@@ -160,6 +176,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
handler = null;
|
||||
|
||||
handlerServer.stop(this);
|
||||
handlerServer = null;
|
||||
|
||||
@@ -192,6 +210,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
|
||||
recordingStatusObservable = null;
|
||||
gpsStatusObservable = null;
|
||||
recordingDataObservable = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
@@ -208,6 +227,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
return recordingStatus.getTrackId();
|
||||
}
|
||||
|
||||
//TODO
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
if (trackStatisticsUpdater == null) {
|
||||
return null;
|
||||
@@ -352,6 +372,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
remoteSensorManager = new BluetoothRemoteSensorManager(this);
|
||||
remoteSensorManager.start();
|
||||
|
||||
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
||||
|
||||
altitudeSumManager = new AltitudeSumManager();
|
||||
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
|
||||
*/
|
||||
private void endRecording(boolean trackStopped) {
|
||||
handler.removeCallbacks(updateRecordingData);
|
||||
if (!trackStopped) {
|
||||
updateRecordingDataWhileRecording();
|
||||
} else {
|
||||
recordingDataObservable.postValue(NOT_RECORDING);
|
||||
}
|
||||
|
||||
// Shutdown periodic tasks
|
||||
voiceExecutor.shutdown();
|
||||
|
||||
@@ -613,14 +642,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
trackPoint.setAltitudeLoss(getAltitudeLoss_m());
|
||||
altitudeSumManager.reset();
|
||||
}
|
||||
|
||||
//TODO We need a copy of the SensorDataSet as data will be reset!
|
||||
SensorDataSet sensorDataSet = fillWithSensorDataSet(trackPoint);
|
||||
if (remoteSensorManager != null) {
|
||||
fillWithSensorDataSet(trackPoint);
|
||||
remoteSensorManager.reset();
|
||||
}
|
||||
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
|
||||
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
|
||||
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
recordingDataObservable.postValue(new RecordingData(track, trackPoint, sensorDataSet));
|
||||
|
||||
contentProviderUtils.updateTrack(track);
|
||||
} catch (SQLiteException e) {
|
||||
/*
|
||||
@@ -632,25 +665,26 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
voiceExecutor.update();
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
private SensorDataSet fillWithSensorDataSet(TrackPoint trackPoint) {
|
||||
if (remoteSensorManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return remoteSensorManager.getSensorData();
|
||||
}
|
||||
SensorDataSet sensorData = remoteSensorManager.getSensorData();
|
||||
if (sensorData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fillWithSensorDataSet(TrackPoint trackPoint) {
|
||||
SensorDataSet sensorData = getSensorDataSet();
|
||||
if (sensorData != null) {
|
||||
if (trackPoint != null) {
|
||||
sensorData.fillTrackPoint(trackPoint);
|
||||
}
|
||||
return sensorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative altitude gain (since last trackpoint).
|
||||
*/
|
||||
public Float getAltitudeGain_m() {
|
||||
private Float getAltitudeGain_m() {
|
||||
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
||||
return null;
|
||||
}
|
||||
@@ -661,7 +695,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
/**
|
||||
* Returns the relative altitude loss (since last trackpoint).
|
||||
*/
|
||||
public Float getAltitudeLoss_m() {
|
||||
private Float getAltitudeLoss_m() {
|
||||
if (altitudeSumManager == null || !altitudeSumManager.isConnected()) {
|
||||
return null;
|
||||
}
|
||||
@@ -712,6 +746,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
||||
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() {
|
||||
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();
|
||||
|
||||
@Deprecated //TODO Implement proper interface that passes the current TrackRecordingService
|
||||
private final Runnable callback;
|
||||
|
||||
private TrackRecordingService trackRecordingService;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.Log;
|
||||
import java.time.Duration;
|
||||
|
||||
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.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
@@ -57,7 +58,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
||||
private boolean started = false;
|
||||
|
||||
private final SharedPreferences sharedPreferences;
|
||||
private int preferenceWheelCircumference;
|
||||
private Distance preferenceWheelCircumference;
|
||||
|
||||
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
|
||||
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
||||
|
||||
@@ -138,11 +138,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
|
||||
@Override
|
||||
public void run(@NonNull TrackRecordingService trackRecordingService) {
|
||||
if (trackRecordingService == null) {
|
||||
Log.e(TAG, "TrackRecordingService is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
|
||||
}
|
||||
|
||||
@@ -183,7 +178,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
}
|
||||
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);
|
||||
ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
while (trackPointIterator.hasNext()) {
|
||||
|
||||
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.settings;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
@@ -93,6 +95,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
|
||||
public static class PrefsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private TextToSpeech tts;
|
||||
private int ttsInitStatus;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
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.
|
||||
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
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
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
|
||||
return directory != null ? directory.getName() : getString(R.string.not_set);
|
||||
});
|
||||
|
||||
voiceAnnouncements = findPreference(getString(R.string.voice_frequency_key));
|
||||
updateVoiceAnnouncements();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -208,6 +232,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection.unbind(getContext());
|
||||
sharedPreferences = null;
|
||||
voiceAnnouncements = null;
|
||||
tts = null;
|
||||
}
|
||||
|
||||
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) {
|
||||
this.recordingStatus = status;
|
||||
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 java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
@@ -98,10 +99,13 @@ public class TrackStatisticsUpdater {
|
||||
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
|
||||
*/
|
||||
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
|
||||
@@ -159,7 +163,7 @@ public class TrackStatisticsUpdater {
|
||||
|
||||
if (!trackPoint.hasSensorDistance()) {
|
||||
// GPS-based distance/speed
|
||||
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
|
||||
Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
|
||||
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
|
||||
speedBuffer_mps.reset();
|
||||
lastTrackPoint = trackPoint;
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.ContextualActionModeCallback;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackController;
|
||||
|
||||
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();
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
@@ -97,47 +96,12 @@ public class ActivityUtils {
|
||||
} else {
|
||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||
}
|
||||
searchView.setQueryRefinementEnabled(true);
|
||||
searchView.setSubmitButtonEnabled(true);
|
||||
searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
return searchView;
|
||||
}
|
||||
|
||||
public static void vibrate(Activity activity, int milliseconds) {
|
||||
final Vibrator vibrator = (Vibrator) activity.getSystemService(VIBRATOR_SERVICE);
|
||||
public static void vibrate(Context context, int milliseconds) {
|
||||
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
} else {
|
||||
|
||||
@@ -142,9 +142,9 @@ public class PreferencesUtils {
|
||||
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));
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<?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" />
|
||||
<foreground>
|
||||
<inset
|
||||
android:inset="20dp"
|
||||
android:drawable="@drawable/ic_logo_color_24dp" />
|
||||
</foreground>
|
||||
</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
|
||||
android:width="300dp"
|
||||
android:height="300dp"
|
||||
android:drawable="@drawable/ic_logo_color_108dp"
|
||||
android:gravity="center_vertical|center_horizontal"
|
||||
tools:targetApi="m" />
|
||||
android:gravity="center"
|
||||
tools:targetApi="m">
|
||||
<inset
|
||||
android:drawable="@drawable/ic_logo_color_24dp"
|
||||
android:inset="20dp" />
|
||||
</item>
|
||||
|
||||
</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:orientation="horizontal">
|
||||
|
||||
<include
|
||||
android:id="@+id/track_controller_container"
|
||||
layout="@layout/track_controller" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/controller_fragment"
|
||||
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/track_list"
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<include
|
||||
android:id="@+id/track_controller_container"
|
||||
layout="@layout/track_controller" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/controller_fragment"
|
||||
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
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
|
||||
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_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -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:layout_width="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 -->
|
||||
<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>
|
||||
</RelativeLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/track_controller_container"
|
||||
layout="@layout/track_controller" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/controller_fragment"
|
||||
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
@@ -17,7 +17,9 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<include
|
||||
android:id="@+id/track_controller_container"
|
||||
layout="@layout/track_controller" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/controller_fragment"
|
||||
android:name="de.dennisguse.opentracks.ControllerFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
@@ -20,14 +20,14 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_statistics_24dp"
|
||||
android:title="@string/menu_aggregated_statistics"
|
||||
app:showAsAction="ifRoom" />
|
||||
<!-- Icon and title is overwritten by TrackListActivity -->
|
||||
<!-- Hide icon as workaround for https://github.com/OpenTracksApp/OpenTracks/issues/200 -->
|
||||
<!-- Icon, title, and visibility is managed by TrackListActivity -->
|
||||
<item
|
||||
android:id="@+id/track_list_start_gps"
|
||||
android:icon="@drawable/ic_gps_off_24dp"
|
||||
android:title="@string/menu_start_gps"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
|
||||
<item
|
||||
android:id="@+id/track_list_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_water_skiing">esquí acuático</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_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_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_average_speed">Velocidad promedio: %1$.2f km/h (%2$.1f mi/h)</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_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>
|
||||
@@ -81,31 +81,31 @@
|
||||
<string name="description_location_latitude">Latitud (grados)</string>
|
||||
<string name="description_location_longitude">Longitud (grados)</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_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_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_moving_time">Tiempo en movimiento: %1$s.</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_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_grade">Pendiente mín.: %1$d %%</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_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_heart_rate">Frecuencia cardíaca (lpm)</string>
|
||||
<string name="description_sensor_power">Potencia (W)</string>
|
||||
<string name="description_speed_imperial">Velocidad (mi/h)</string>
|
||||
<string name="description_speed_metric">Velocidad (km/h)</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_time">Tiempo total: %1$s.</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_track_point">Punto</string>
|
||||
<string name="description_track_segment">Segmento</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_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_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="files_few">%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_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_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_wait_for_better_signal">Esperando una mejor 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_stop">Parar</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_no_directory">%1$s no existe.</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_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_progress_message">Importando desde %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_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_success">Se insertó un 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_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente.</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_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_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_start">%1$s (Inicio)</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_edit_marker_type">Tipo de marcador (p. ej., cascada)</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_delete">Eliminar</string>
|
||||
<string name="menu_edit">Editar</string>
|
||||
@@ -196,17 +196,17 @@
|
||||
<string name="menu_resume_track">Reanudar ruta</string>
|
||||
<string name="recording_service">Servicio de grabación</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_marker_track_location">En el recorrido “%1$s”</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="sensor_state_battery">Nivel de la batería</string>
|
||||
<string name="sensor_state_cadence">Cadencia</string>
|
||||
<string name="sensor_state_cadence_value">%1$d rpm</string>
|
||||
<string name="sensor_state_disconnected">Desconectado</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_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_advanced">Opciones avanzadas</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_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_title">Reanudación autom.</string>
|
||||
<string name="settings_recording_default_activity_title">Actividad predet.</string>
|
||||
<string name="settings_recording_auto_resume_track_timeout_title">Reanudación automática</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_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_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_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_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_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_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_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_bluetooth">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_title">Unidades preferidas</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="stats_average_moving_pace">Ritmo 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_longitude">Longitud</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_moving_time">Tiempo en movimiento</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_hint">Tipo de actividad</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_many">%1$d recorridos</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_mile">%1$d millas</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_second">%1$d s</string>
|
||||
<string name="value_none">Ninguno</string>
|
||||
@@ -416,4 +418,145 @@
|
||||
<!-- Share image -->
|
||||
<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>
|
||||
</resources>
|
||||
<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>
|
||||