Compare commits

...

37 Commits

Author SHA1 Message Date
Dennis Guse ae3078304d Release: v3.18.0 2021-05-08 14:53:40 +02:00
Dennis Guse 48b6438c06 Bugfix: TrackDataHub needs to use the same TrackStatisticsUpdater for a whole track (incl. newly received TrackPoints).
Fixes #745.

Introduced in https://github.com/OpenTracksApp/OpenTracks/commit/887d4700d4f8fbf10563834e7498388a3213007e
2021-05-08 14:40:32 +02:00
Dennis Guse e822172f58 Cleanup. 2021-05-08 14:40:32 +02:00
Dennis Guse a3aee403a1 CI: run tests on more API Android levels. 2021-05-06 19:33:18 +02:00
Dennis Guse aac2ad4165 Removed invalid Russian translation (ImpliedQuantity). 2021-05-06 18:33:03 +02:00
mondstern 76d5e5780c Translated using Weblate (Finnish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-05-06 18:07:44 +02:00
mondstern 931fd7514a Translated using Weblate (Basque)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/eu/
2021-05-06 18:07:44 +02:00
mondstern d1570ad700 Translated using Weblate (Estonian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-05-06 18:07:44 +02:00
mondstern 04fff89f68 Translated using Weblate (Danish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/da/
2021-05-06 18:07:44 +02:00
mondstern b57fd368ae Translated using Weblate (Russian)
Currently translated at 100.0% (529 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-06 18:07:44 +02:00
chipechop 58d6a3b5a9 Update strings.xml
Hi, 
I'm new, here.

I changed some translations that didn't fit or were not correct (hr is h in italian).

I'm enthusiast of your app: it's just perfect.

Cheers!
2021-05-06 18:07:39 +02:00
dependabot[bot] 289a5273a9 Bump mockito-android from 3.8.0 to 3.9.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.8.0...v3.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 07:54:02 +02:00
Dennis Guse 120e20a0ea Avoid crash on older Android when stopping a track. 2021-05-05 21:50:26 +02:00
Dennis Guse 2843d19450 UI presents altitude in EGM2008. 2021-05-05 18:01:07 +02:00
Dennis Guse 50127e311a Introduced value object for Altitude. 2021-05-05 18:01:07 +02:00
Dennis Guse f83a1b3a4c EGM2008: add to UI in StatisticsRecordingFragment. 2021-05-05 18:01:07 +02:00
Dennis Guse 65ecbc4521 EGM2008: add reference to about activity. 2021-05-05 18:01:07 +02:00
Dennis Guse fa50db08c3 EGM2008: added undulation data and bilinear computation.
Includes data structure for caching.

Part of #224.
2021-05-05 18:01:07 +02:00
Dennis Guse b395c19832 Bugfix: voice announcements need to use KM/Miles (instead of m/feet).
Fixes #722.
2021-05-05 17:59:08 +02:00
Dennis Guse 7e6ffbabd2 Build: replace JCenter with MavenCentral; requires upgrade to AndroidStudio 4.2.X.
Fixes #653.
2021-05-05 12:45:23 +02:00
dependabot[bot] 6b43d341d0 Bump gradle from 4.1.3 to 4.2.0
Bumps gradle from 4.1.3 to 4.2.0.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-05 12:02:45 +02:00
Dennis Guse 8af0fb3250 Bugfix: TrackDataHub did not account for recordingDistanceInterval.
Introduced in 887d4700d4
2021-05-03 22:13:23 +02:00
Dennis Guse 49219de46a GPSStatus: update initial and final status. 2021-05-03 22:13:23 +02:00
Dennis Guse 1baf652b81 Bugfix: ChartFragment needs to show elevation in M/FT. 2021-05-03 22:13:23 +02:00
Dennis Guse 6723bfb979 TrackRecordingService: provide status updates via LiveData.
Fixes #719
2021-05-03 22:13:23 +02:00
Nikita Epifanov 8357b234d2 Translated using Weblate (Russian)
Currently translated at 81.4% (431 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-01 20:03:23 +02:00
mondstern 9de9fbd058 Translated using Weblate (Russian)
Currently translated at 81.2% (430 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2021-05-01 20:03:23 +02:00
Dennis Guse 6da6fc6004 Upgrade to Gradle 7.0 2021-05-01 20:03:19 +02:00
Dennis Guse 114fc31be0 KmzImporter: process doc.kml as stream (instead of in memory copy).
Also simplified exception handling.
2021-05-01 20:01:06 +02:00
Dennis Guse ae795a76fa Removed TrackImporter interface. 2021-05-01 20:01:06 +02:00
Dennis Guse 92004b0ec7 Cleanup: refactored importers. 2021-05-01 20:01:06 +02:00
Dennis Guse f277347529 ExportImportTest: be realistic by creating actual files. 2021-05-01 20:01:06 +02:00
Dennis Guse ad1aa06407 Cleanup: extracted SAX2 import from AbstractFileTrackImporter. 2021-05-01 20:01:06 +02:00
Dennis Guse 3b92303511 Cleanup: move GPX-specific importer code to GPX importer. 2021-05-01 20:01:06 +02:00
Dennis Guse e62faa1253 Reproducible build: disable PNG crunching.
Part of #418.
2021-05-01 19:27:25 +02:00
dependabot[bot] d3900f4dad Bump espresso-core from 3.4.0-alpha05 to 3.4.0-alpha06
Bumps espresso-core from 3.4.0-alpha05 to 3.4.0-alpha06.

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 12:09:04 +02:00
badger40 00b8834795 Update README_TESTED_SENSORS.md 2021-04-28 07:41:00 +02:00
70 changed files with 39601 additions and 915 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
strategy: strategy:
matrix: matrix:
api-level: [29] api-level: [ 23, 26, 29 ]
target: [default] target: [default]
steps: steps:
- name: checkout - name: checkout
+2
View File
@@ -28,6 +28,8 @@ However, often only one value is provided.
* Wahoo Speed (Model: WFRPMSPD) * Wahoo Speed (Model: WFRPMSPD)
* CooSpo cadence and speed sensor (model: BK467)
### Cadence ### Cadence
* Garmin Cadence Sensor 2 * Garmin Cadence Sensor 2
+11 -7
View File
@@ -3,17 +3,17 @@ apply plugin: 'com.android.application'
buildscript { buildscript {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.android.tools.build:gradle:4.2.0'
} }
} }
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }
@@ -49,10 +49,14 @@ android {
viewBinding = true viewBinding = true
} }
aaptOptions {
cruncherEnabled = false
}
defaultConfig { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 4009 versionCode 4046
versionName "v3.17.3" versionName "v3.18.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,7 +90,7 @@ android {
} }
dependencies { dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha05' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
@@ -99,5 +103,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.8.0' androidTestImplementation 'org.mockito:mockito-android:3.9.0'
} }
@@ -0,0 +1,15 @@
v3.18.0: OpenTracks
Changes:
- Altitude is shown in EGM2008 (stored/exported as WGS84)
Bugfix:
- Fixed memory leak (introduced in v3.17.0)
- Charts: altitude should be in M/FT (introduced in v3.17.1)
- Charts: while recording was not correctly updated (introduced in v3.17.3)
- TrackDataHub did not account for recordingDistanceInterval (introduced in v3.17.2)
Developer:
- Disable PNG crunching (reproducible build)
- Refactored TrackImporter and optimized KMZ import
- Use LiveData for TrackRecordingService to UI communication (removing memory leak)
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
Vendored
+1
View File
@@ -84,6 +84,7 @@ set CMD_LINE_ARGS=%*
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
@@ -54,7 +54,7 @@ public class ChartPointTest {
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true); ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
// then // then
assertEquals(0.05, point.getAltitude(), 0.01); assertEquals(50, point.getAltitude(), 0.01);
assertEquals(36, point.getSpeed(), 0.01); assertEquals(36, point.getSpeed(), 0.01);
assertEquals(1.66, point.getPace(), 0.01); assertEquals(1.66, point.getPace(), 0.01);
} }
@@ -28,7 +28,6 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.Distance;
/** /**
* Tests {@link ChartFragment}. * Tests {@link ChartFragment}.
@@ -59,7 +58,6 @@ public class ChartFragmentTest {
boolean chartByDistance = false; boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance); chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance)); chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
} }
@Test @Test
@@ -3,24 +3,26 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.junit.After; import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream; import java.io.File;
import java.io.ByteArrayOutputStream; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
@@ -30,6 +32,7 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; 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.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.Speed;
@@ -41,7 +44,6 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter; import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -53,7 +55,7 @@ import static org.junit.Assert.assertTrue;
* Export a track to {@link TrackFileFormat} and verify that the import is identical. * Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p> * <p>
*/ */
@RunWith(JUnit4.class) @RunWith(AndroidJUnit4.class)
public class ExportImportTest { public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName(); private static final String TAG = ExportImportTest.class.getSimpleName();
@@ -71,7 +73,6 @@ public class ExportImportTest {
} }
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -79,6 +80,9 @@ public class ExportImportTest {
private static final String TRACK_CATEGORY = "the category"; private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description"; private static final String TRACK_DESCRIPTION = "the description";
private File tmpFile;
private Uri tmpFileUri;
private Track track; private Track track;
private List<Marker> markers = new ArrayList<>(); private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>(); private List<TrackPoint> trackPoints = new ArrayList<>();
@@ -86,6 +90,18 @@ public class ExportImportTest {
private Track.Id trackId; private Track.Id trackId;
private Track.Id importTrackId; private Track.Id importTrackId;
@Before
public void fileSetup() throws IOException {
tmpFile = File.createTempFile("test", "test", context.getFilesDir());
tmpFileUri = Uri.fromFile(tmpFile);
}
@After
public void FileTearDown() {
tmpFile.deleteOnExit();
tmpFileUri = null;
}
public void setUp(boolean hasSensorDistance) throws TimeoutException { public void setUp(boolean hasSensorDistance) throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))) TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService(); .getService();
@@ -138,7 +154,7 @@ public class ExportImportTest {
@LargeTest @LargeTest
@Test @Test
public void kml_with_trackdetail() throws TimeoutException { public void kml_with_trackdetail() throws TimeoutException, IOException {
setUp(false); setUp(false);
// given // given
@@ -148,13 +164,12 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(track, outputStream);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context); XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0); importTrackId = trackImporter.importFile(inputStream).get(0);
// then // then
@@ -179,7 +194,7 @@ public class ExportImportTest {
@LargeTest @LargeTest
@Test @Test
public void kml_with_trackdetail_and_sensordata() throws TimeoutException { public void kml_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true); setUp(true);
// given // given
@@ -189,13 +204,12 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(track, outputStream);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context); XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0); importTrackId = trackImporter.importFile(inputStream).get(0);
// then // then
@@ -217,9 +231,48 @@ public class ExportImportTest {
assertMarkers(); assertMarkers();
} }
//TODO Does not test images
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackExporter trackExporter = TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
KmzTrackImporter trackImporter = new KmzTrackImporter();
importTrackId = trackImporter.importFile(context, tmpFileUri).get(0);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
assertTrackpoints(trackPoints, true, true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics(false, true, true);
// 4. markers
assertMarkers();
}
@LargeTest @LargeTest
@Test(expected = ImportAlreadyExistsException.class) @Test(expected = ImportAlreadyExistsException.class)
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException { public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false); setUp(false);
// given // given
@@ -231,13 +284,11 @@ public class ExportImportTest {
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context); TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when // when
// 1. export trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
// 2. import // 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context); XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0); importTrackId = trackImporter.importFile(inputStream).get(0);
// then // then
@@ -247,7 +298,7 @@ public class ExportImportTest {
@LargeTest @LargeTest
@Test @Test
public void gpx() throws TimeoutException { public void gpx() throws TimeoutException, IOException {
setUp(true); setUp(true);
// given // given
@@ -257,13 +308,12 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(track, outputStream);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils); XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0); importTrackId = trackImporter.importFile(inputStream).get(0);
// then // then
@@ -295,7 +345,7 @@ public class ExportImportTest {
@LargeTest @LargeTest
@Test(expected = ImportAlreadyExistsException.class) @Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException { public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false); setUp(false);
// given // given
@@ -308,12 +358,11 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
trackExporter.writeTrack(track, outputStream);
// 2. import // 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils); XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0); importTrackId = trackImporter.importFile(inputStream).get(0);
// then // then
@@ -368,7 +417,7 @@ public class ExportImportTest {
} }
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude()); assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001); assertEquals(trackPoint.getAltitude().toM(), importedTrackPoint.getAltitude().toM(), 0.001);
} }
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) { if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
@@ -438,7 +487,7 @@ public class ExportImportTest {
} }
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) { private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, Distance distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time)); TrackPoint tp = new TrackPoint(latitude, longitude, Altitude.WGS84.of(altitude), Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy); tp.setAccuracy(accuracy);
tp.setSpeed(Speed.of(speed)); tp.setSpeed(Speed.of(speed));
tp.setHeartRate_bpm(heartRate); tp.setHeartRate_bpm(heartRate);
@@ -11,6 +11,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
@@ -48,9 +49,9 @@ public class LegacyImportTest {
*/ */
@LargeTest @LargeTest
@Test @Test
public void kml_with_statistics_marker() { public void kml_with_statistics_marker() throws IOException {
// given // given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context); 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); InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_statistics_marker);
// when // when
@@ -90,9 +91,9 @@ public class LegacyImportTest {
*/ */
@LargeTest @LargeTest
@Test(expected = ImportParserException.class) @Test(expected = ImportParserException.class)
public void kml_without_locations() { public void kml_without_locations() throws IOException {
// given // given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context); XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty); InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml_empty);
// when // when
@@ -104,9 +105,9 @@ public class LegacyImportTest {
*/ */
@LargeTest @LargeTest
@Test @Test
public void gpx_with_pause_resume() { public void gpx_with_pause_resume() throws IOException {
// given // given
GpxFileTrackImporter trackImporter = new GpxFileTrackImporter(context); XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume); InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
// when // when
@@ -151,7 +152,7 @@ public class LegacyImportTest {
if (altitude == null) { if (altitude == null) {
assertFalse(trackPoint.hasAltitude()); assertFalse(trackPoint.hasAltitude());
} else { } else {
assertEquals(altitude, (Double) trackPoint.getAltitude()); assertEquals(altitude, (Double) trackPoint.getAltitude().toM());
} }
} }
@@ -31,6 +31,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
@@ -83,6 +84,10 @@ public class TrackRecordingServiceTest {
if (Looper.myLooper() == null) Looper.prepare(); if (Looper.myLooper() == null) Looper.prepare();
} }
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -13,6 +13,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
@@ -44,6 +45,7 @@ import static org.junit.Assert.assertNotNull;
* So, if it is executed on a real device, data might be lost. * So, if it is executed on a real device, data might be lost.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
//TODO Implement as mock test; no need to store data in database
public class TrackRecordingServiceTestLocation { public class TrackRecordingServiceTestLocation {
@Rule @Rule
@@ -63,6 +65,11 @@ public class TrackRecordingServiceTestLocation {
if (Looper.myLooper() == null) Looper.prepare(); if (Looper.myLooper() == null) Looper.prepare();
} }
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before @Before
public void setUp() throws TimeoutException { public void setUp() throws TimeoutException {
// Set up the mock content resolver // Set up the mock content resolver
@@ -1,59 +0,0 @@
/*
* Copyright 2010 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.services.tasks;
import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link AnnouncementPeriodicTaskFactory}.
*
* @author Rodrigo Damazio
*/
@RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest {
private final Context context = ApplicationProvider.getApplicationContext();
@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();
}
@Test
public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(context);
assertTrue(task instanceof AnnouncementPeriodicTask);
}
}
@@ -0,0 +1,69 @@
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class PeriodicTaskExecutorTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@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();
}
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void calculateNextTaskDistance() throws TimeoutException {
// given
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
PeriodicTaskExecutor periodicTaskExecutor = new PeriodicTaskExecutor(service, new AnnouncementPeriodicTask.Factory());
periodicTaskExecutor.setMetricUnits(true);
periodicTaskExecutor.setTaskFrequency(-5);
// when
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(13000));
assertEquals(Distance.of(15000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
statistics.setTotalDistance(Distance.of(15100));
assertEquals(Distance.of(20000), periodicTaskExecutor.calculateNextTaskDistance(statistics));
}
}
@@ -9,6 +9,7 @@ import org.junit.runner.RunWith;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -53,8 +54,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(); TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
// when // when
subject.addTrackPoint(tp1, GPS_DISTANCE); subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -71,8 +72,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(); TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f)); tp3.setSpeed(Speed.of(5f));
// when // when
@@ -90,11 +91,11 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(); TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f)); tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp2.setSpeed(Speed.of(5f)); tp2.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000)); TrackPoint tp4 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp2.setSpeed(Speed.of(5f)); tp2.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f)); tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000)); TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
@@ -122,9 +123,9 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater(); TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000)); TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSensorDistance(Distance.of(5f)); tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000)); TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f)); tp5.setSensorDistance(Distance.of(10f));
@@ -0,0 +1,229 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@RunWith(JUnit4.class)
public class EGM2008UtilsTest {
private static final double MAX_BILINEAR_ERROR = 0.478;
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void fileVerification() throws IOException {
// given
int expectedLength = 18671444;
int expectedHeaderLength = 404;
String expectedHeader = "P5\n" +
"# Geoid file in PGM format for the GeographicLib::Geoid class\n" +
"# Description WGS84 EGM2008, 5-minute grid\n" +
"# URL http://earth-info.nga.mil/GandG/wgs84/gravitymod/egm2008\n" +
"# DateTime 2009-08-29 18:45:00\n" +
"# MaxBilinearError 0.478\n" +
"# RMSBilinearError 0.012\n" +
"# MaxCubicError 0.294\n" +
"# RMSCubicError 0.005\n" +
"# Offset -108\n" +
"# Scale 0.003\n" +
"# Origin 90N 0E\n" +
"# AREA_OR_POINT Point\n" +
"# Vertical_Datum WGS84\n" +
"4320 2161\n" +
"65535" +
"\n";
// when
try (InputStream inputStream = context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA)) {
assertEquals(expectedLength, inputStream.available());
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII);
char[] data = new char[expectedHeaderLength];
int length = reader.read(data);
// then
assertEquals(expectedHeaderLength, length);
assertEquals(expectedHeader, new String(data));
}
}
@Test
public void data_Northpole() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(90);
trackPoint.setLongitude(0.1);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-14.8980, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(0);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole2() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(180);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Southpole3() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(30.15, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_0() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(0);
trackPoint.setLongitude(0);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-17.2260, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Berlin() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(52.530644);
trackPoint.setLongitude(13.383068);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-39.4865, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_Berlin_Caching() throws IOException {
// given
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint1.setLatitude(52.530644);
trackPoint1.setLongitude(13.383068);
trackPoint1.setAltitude(0);
TrackPoint trackPoint2 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint2.setLatitude(52.530000);
trackPoint2.setLongitude(13.380000);
trackPoint2.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint1.getLocation());
// then
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
}
@Test
public void data_MaxUndulation() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(-8.417);
trackPoint.setLongitude(147.367);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void getIndices() {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(90);
trackPoint.setLongitude(0);
assertEquals(new EGM2008Utils.Indices(0, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(0);
trackPoint.setLongitude(0);
assertEquals(new EGM2008Utils.Indices(1080, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(180);
assertEquals(new EGM2008Utils.Indices(2160, 4320 / 2), EGM2008Utils.getIndices(trackPoint.getLocation()));
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
}
@Test
public void getUndulationRaw_ok() throws IOException {
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
assertEquals(40966, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(0, 0)));
assertEquals(41742, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(1081, 0)));
assertEquals(25950, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2160, 4319)));
}
}
@Test(expected = EOFException.class)
public void getUndulationRaw_error() throws IOException {
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008Utils.EGM2008_5_DATA))) {
assertEquals(0, EGM2008Utils.getUndulationRaw(dataInputStream, new EGM2008Utils.Indices(2161, 4320)), 0.01);
}
}
}
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -36,6 +38,8 @@ import de.dennisguse.opentracks.util.IntentUtils;
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
//TODO It is actually not a List; we need a better name for this class
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller { public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
private static final String TAG = AbstractListActivity.class.getSimpleName(); private static final String TAG = AbstractListActivity.class.getSimpleName();
@@ -43,8 +47,6 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
protected static final int GPS_REQUEST_CODE = 6; protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3; private static final int DELETE_REQUEST_CODE = 3;
protected Track.Id recordingTrackId;
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) { if (requestCode == DELETE_REQUEST_CODE) {
@@ -68,7 +70,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
boolean stopRecording = false; boolean stopRecording = false;
for (Track.Id trackId : trackIds) { for (Track.Id trackId : trackIds) {
if (trackId.equals(recordingTrackId)) { if (trackId.equals(getRecordingTrackId())) {
stopRecording = true; stopRecording = true;
break; break;
} }
@@ -87,10 +89,17 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
* Gets the track recording service connection. * Gets the track recording service connection.
* For stopping the current recording if need to delete the current recording track. * For stopping the current recording if need to delete the current recording track.
*/ */
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection(); protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return null;
}
/** /**
* Called after {@link TrackDeleteActivity} returns its result. * Called after {@link TrackDeleteActivity} returns its result.
*/ */
abstract protected void onTrackDeleted(); abstract protected void onTrackDeleted();
@Nullable
protected Track.Id getRecordingTrackId() {
return null;
}
} }
@@ -20,6 +20,7 @@ import android.app.SearchManager;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -42,7 +43,6 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.util.ActivityUtils; import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
@@ -51,7 +51,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller, TrackRecordingServiceStatus.Listener { public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
@@ -59,8 +59,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private Track.Id recordingTrackId; private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private boolean recordingTrackPaused;
private Track track; private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter; private MarkerResourceCursorAdapter resourceCursorAdapter;
@@ -74,9 +74,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override @Override
public void run() { public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) { if (service == null) {
service.addListener(MarkerListActivity.this); Log.w(TAG, "could not get TrackRecordingService");
return;
} }
service.getRecordingStatusObservable()
.observe(MarkerListActivity.this, status -> onRecordingStatusChanged(status));
} }
}; };
@@ -178,7 +182,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingTrackId) && !recordingTrackPaused); insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()) && !recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -290,6 +294,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
// Do nothing // Do nothing
} }
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> { private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null; private String searchQuery = null;
@@ -344,14 +352,4 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter.swapCursor(null); resourceCursorAdapter.swapCursor(null);
} }
} }
@Override
public void onTrackRecordingPaused(boolean isPaused) {
recordingTrackPaused = isPaused;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
} }
@@ -20,6 +20,7 @@ import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.graphics.drawable.TransitionDrawable; import android.graphics.drawable.TransitionDrawable;
import android.os.Handler; import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
@@ -37,6 +38,8 @@ import de.dennisguse.opentracks.util.StringUtils;
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
//TODO Subscribe to TrackRecordingService.recordingStatus
//TODO Could be a fragment
public class TrackController implements View.OnTouchListener { public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName(); private static final String TAG = TrackController.class.getSimpleName();
@@ -54,17 +57,16 @@ public class TrackController implements View.OnTouchListener {
private final TrackControllerBinding viewBinding; private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition; private final TransitionDrawable transition;
private boolean isRecording;
private boolean isRecordingPaused; private TrackRecordingService.RecordingStatus recordingStatus;
private Duration totalTime; private Duration totalTime;
private boolean isResumed = false;
private final Callback callback; private final Callback callback;
// A runnable to update the total time. // A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() { private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() { public void run() {
if (isResumed && isRecording && !isRecordingPaused) { if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
updateTotalTime(); updateTotalTime();
setTotalTime(); setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis()); handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
@@ -84,7 +86,7 @@ public class TrackController implements View.OnTouchListener {
viewBinding.trackControllerRecord.setOnTouchListener(this); viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> { viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isRecordingPaused)) { if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return; return;
} }
@@ -98,6 +100,10 @@ public class TrackController implements View.OnTouchListener {
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually. @SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override @Override
public boolean onTouch(final View view, MotionEvent event) { 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) { if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false; return false;
} }
@@ -120,7 +126,7 @@ public class TrackController implements View.OnTouchListener {
//To pause a recording //To pause a recording
if (viewBinding.trackControllerRecord.equals(view) if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN && event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isRecordingPaused) { && recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration); transition.startTransition(buttonDelayDuration);
@@ -143,7 +149,7 @@ public class TrackController implements View.OnTouchListener {
//To stop a recording //To stop a recording
if (viewBinding.trackControllerStop.equals(view) if (viewBinding.trackControllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN && event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording) { && recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration); transition.startTransition(buttonDelayDuration);
@@ -165,13 +171,15 @@ public class TrackController implements View.OnTouchListener {
return false; return false;
} }
public void update(boolean recording, boolean paused) { public void update(TrackRecordingService.RecordingStatus recordingStatus) {
if (!isResumed) { if (!isResumed()) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return; return;
} }
isRecording = recording;
isRecordingPaused = paused; this.recordingStatus = recordingStatus;
boolean isRecording = recordingStatus.isRecording();
boolean isRecordingPaused = recordingStatus.isPaused();
boolean visible = alwaysShow || isRecording; boolean visible = alwaysShow || isRecording;
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE); viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
@@ -197,19 +205,19 @@ public class TrackController implements View.OnTouchListener {
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) { if (recordingStatus.isRecording()) {
updateTotalTime(); updateTotalTime();
setTotalTime(); setTotalTime();
} }
} }
void onResume(boolean recording, boolean paused) { void onResume(TrackRecordingService.RecordingStatus recordingStatus) {
isResumed = true; this.recordingStatus = recordingStatus;
update(recording, paused); update(recordingStatus);
} }
void onPause() { void onPause() {
isResumed = false; recordingStatus = null;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
} }
@@ -235,12 +243,18 @@ public class TrackController implements View.OnTouchListener {
} }
private void showStatusSetDefaultText() { private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(isRecordingPaused ? R.string.generic_paused : R.string.generic_recording); viewBinding.trackControllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private boolean isResumed() {
return recordingStatus != null;
} }
public interface Callback { public interface Callback {
void recordStart(); void recordStart();
void recordPause(); void recordPause();
void recordStop(); void recordStop();
} }
} }
@@ -38,6 +38,7 @@ import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
@@ -55,7 +56,6 @@ import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue; import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils; import de.dennisguse.opentracks.util.ActivityUtils;
@@ -71,7 +71,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback, TrackRecordingServiceStatus.Listener { public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
private static final String TAG = TrackListActivity.class.getSimpleName(); private static final String TAG = TrackListActivity.class.getSimpleName();
@@ -80,7 +80,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController; private TrackController trackController;
private ResourceCursorAdapter resourceCursorAdapter; private ResourceCursorAdapter resourceCursorAdapter;
private GpsStatusValue gpsStatusValue;
private TrackListBinding viewBinding; private TrackListBinding viewBinding;
@@ -89,7 +88,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Preferences // Preferences
private boolean metricUnits = true; private boolean metricUnits = true;
private boolean recordingTrackPaused = true; private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
// Callback when an item is selected in the contextual action mode // Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -118,7 +118,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(() -> { runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu(); TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart(); loaderCallbacks.restart();
trackController.update(isRecording(), recordingTrackPaused); trackController.onResume(recordingStatus);
}); });
} }
} }
@@ -133,27 +133,29 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public void run() { public void run() {
// After binding changes (e.g., becomes available), update the total time in trackController. // After binding changes (e.g., becomes available), update the total time in trackController.
runOnUiThread(() -> trackController.update(isRecording(), recordingTrackPaused)); runOnUiThread(() -> trackController.update(recordingStatus));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) { if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording"); Log.e(TAG, "service not available to start gps or a new recording");
gpsStatusValue = GpsStatusValue.GPS_NONE;
return; return;
} }
service.getRecordingStatusObservable()
.observe(TrackListActivity.this, status -> onRecordingStatusChanged(status));
// Get GPS status and listen GPS status changes. // Get GPS status and listen GPS status changes.
gpsStatusValue = service.getGpsStatus(); service.getGpsStatusObservable()
updateGpsMenuItem(true, isRecording()); .observe(TrackListActivity.this, status -> onGpsStatusChanged(status));
service.addListener(TrackListActivity.this);
if (isGpsStarted()) { updateGpsMenuItem(true, recordingStatus.isRecording());
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
return; return;
} }
//TODO Not cool to do this in a callback that might be called more than once!
service.tryStartGps(); service.tryStartGps();
gpsStatusValue = GpsStatusValue.GPS_ENABLED;
updateGpsMenuItem(true, isRecording());
} }
}; };
@@ -165,8 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
gpsStatusValue = GpsStatusValue.GPS_NONE;
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
@@ -181,7 +181,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView); viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> { viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
if (isRecording() && trackId == recordingTrackId.getId()) { if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
// Is recording -> open record activity. // Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class) Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId)); .putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
@@ -211,7 +211,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT); int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
Track.Id trackId = new Track.Id(cursor.getLong(idIndex)); Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
boolean isRecording = trackId.equals(recordingTrackId); boolean isRecording = trackId.equals(recordingStatus.getTrackId());
String icon = cursor.getString(iconIndex); String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon); int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex); String name = cursor.getString(nameIndex);
@@ -222,7 +222,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex); String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex); String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused, ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount, iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
startTime, true, category, description, false); startTime, true, category, description, false);
} }
@@ -265,7 +265,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI // Update UI
this.invalidateOptionsMenu(); this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
trackController.onResume(isRecording(), recordingTrackPaused); trackController.onResume(recordingStatus);
} }
@Override @Override
@@ -306,7 +306,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(isGpsStarted(), isRecording()); updateMenuItems(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView(); SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false); searchView.setQuery("", false);
@@ -322,7 +322,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else { } else {
// Invoke trackRecordingService // Invoke trackRecordingService
if (!isGpsStarted()) { if (!gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this); trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run(); bindChangedCallback.run();
} else { } else {
@@ -413,8 +413,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Do nothing // Do nothing
} }
private boolean isGpsStarted() { @Nullable
return gpsStatusValue != GpsStatusValue.GPS_NONE && gpsStatusValue != GpsStatusValue.GPS_DISABLED; @Override
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
} }
private void requestGPSPermissions() { private void requestGPSPermissions() {
@@ -554,16 +556,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public void recordStart() { public void recordStart() {
if (recordingTrackId == null) { if (recordingStatus.getTrackId() == null) {
// Not recording -> Recording // Not recording -> Recording
updateMenuItems(false, true); updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class); Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent); startActivity(newIntent);
} else if (recordingTrackPaused) { } else if (recordingStatus.isPaused()) {
// Paused -> Resume // Paused -> Resume
updateMenuItems(false, true); updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack(); trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} }
} }
@@ -571,7 +572,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void recordPause() { public void recordPause() {
updateMenuItems(false, true); updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack(); trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
} }
@Override @Override
@@ -580,29 +580,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.stopRecording(TrackListActivity.this); trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
} }
private boolean isRecording() { public void onGpsStatusChanged(GpsStatusValue newStatus) {
return recordingTrackId != null;
}
@Override
public void onGpsStatus(GpsStatusValue newStatus) {
gpsStatusValue = newStatus; gpsStatusValue = newStatus;
updateGpsMenuItem(true, isRecording()); updateGpsMenuItem(true, recordingStatus.isRecording());
} }
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(isRecording(), isPaused);
}
recordingTrackPaused = isPaused;
}
@Override private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
public void onTrackRecordingId(Track.Id trackId) { recordingStatus = status;
if (!isRecording() && trackId != null) {
trackController.update(true, recordingTrackPaused); trackController.update(recordingStatus);
}
recordingTrackId = trackId;
} }
} }
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
@@ -24,6 +25,7 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
@@ -42,10 +44,10 @@ import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* An activity to show the track detail, record a new track or resumes an existing one. * An activity to show the track detail, record a new track or resumes an existing one.
@@ -54,7 +56,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality. //TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface, TrackRecordingServiceStatus.Listener { public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName(); private static final String TAG = TrackRecordedActivity.class.getSimpleName();
@@ -67,11 +69,13 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
// The following are set in onCreate. // The following are set in onCreate.
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private TrackRecordedBinding viewBinding; private TrackRecordedBinding viewBinding;
private Track.Id trackId; private Track.Id trackId;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -79,8 +83,22 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override @Override
public void run() { public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) { if (service == null) {
service.addListener(TrackRecordedActivity.this); Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(TrackRecordedActivity.this, status -> onRecordingStatusChanged(status));
}
};
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.isKey(TrackRecordedActivity.this, R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordedActivity.this));
} }
} }
}; };
@@ -90,6 +108,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
handleIntent(getIntent()); handleIntent(getIntent());
trackDataHub = new TrackDataHub(this); trackDataHub = new TrackDataHub(this);
@@ -110,6 +130,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackDataHub.start(); trackDataHub.start();
} }
@@ -132,6 +156,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onStop(); super.onStop();
trackRecordingServiceConnection.unbind(this); trackRecordingServiceConnection.unbind(this);
trackDataHub.stop(); trackDataHub.stop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
@Override @Override
@@ -162,7 +187,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(recordingTrackId == null); menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : ""); setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
@@ -217,12 +242,13 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Nullable
@Override @Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() { protected Track.Id getRecordingTrackId() {
// Not needed. return recordingStatus.getTrackId();
return null;
} }
@Override @Override
protected void onTrackDeleted() { protected void onTrackDeleted() {
runOnUiThread(this::finish); runOnUiThread(this::finish);
@@ -305,8 +331,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
startPostponedEnterTransition(); startPostponedEnterTransition();
} }
@Override private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
public void onTrackRecordingId(Track.Id trackId) { recordingStatus = status;
recordingTrackId = trackId;
} }
} }
@@ -28,7 +28,6 @@ import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils; import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
@@ -44,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/ */
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key. //NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow. //This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback, TrackRecordingServiceStatus.Listener { public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
@@ -62,20 +61,23 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private TrackRecordingBinding viewBinding; private TrackRecordingBinding viewBinding;
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService // Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
@Deprecated
private Track.Id trackId; private Track.Id trackId;
private boolean recordingTrackPaused;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
// After binding changes (is available), update the total time in trackController.
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) { if (service == null) {
Log.d(TAG, "could not get TrackRecordingService"); Log.w(TAG, "could not get TrackRecordingService");
return; return;
} }
service.getRecordingStatusObservable()
.observe(TrackRecordingActivity.this, status -> onRecordingStatusChanged(status));
if (!service.isRecording()) { if (!service.isRecording()) {
if (trackId == null) { if (trackId == null) {
// trackId isn't initialized -> leads a new recording. // trackId isn't initialized -> leads a new recording.
@@ -87,12 +89,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on. // A recording track is on.
trackDataHub.loadTrack(trackId); trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId); trackDataHub.setRecordingStatus(recordingStatus);
trackController.update(true, false); trackController.onResume(recordingStatus);
trackController.onResume(true, recordingTrackPaused);
} }
service.addListener(TrackRecordingActivity.this);
} }
}; };
@@ -108,11 +108,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) { if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) {
setFullscreenPolicy(); setFullscreenPolicy();
} }
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordingActivity.this));
}
if (key == null) return; if (key == null) return;
runOnUiThread(() -> {
TrackRecordingActivity.this.invalidateOptionsMenu(); runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu);
trackController.update(true, recordingTrackPaused);
});
} }
}; };
@@ -207,13 +208,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
super.onResume(); super.onResume();
// Update UI // Update UI
this.invalidateOptionsMenu(); invalidateOptionsMenu();
if (trackId != null) { if (trackId != null) {
//TODO Pass recordingStatus directly to them
trackDataHub.loadTrack(trackId); trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId); trackDataHub.setRecordingStatus(recordingStatus);
trackDataHub.setRecordingTrackPaused(recordingTrackPaused); trackController.onResume(recordingStatus);
trackController.onResume(true, recordingTrackPaused);
} }
/* /*
@@ -264,7 +265,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingTrackPaused); updateMenuItems(recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -339,14 +340,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
public void recordStart() { public void recordStart() {
updateMenuItems(false); updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack(); trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} }
@Override @Override
public void recordPause() { public void recordPause() {
updateMenuItems(true); updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack(); trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
} }
@Override @Override
@@ -404,24 +403,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
} }
} }
@Override private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
public void onTrackRecordingPaused(boolean isPaused) { recordingStatus = status;
if (recordingTrackPaused != isPaused) {
trackController.update(true, isPaused); trackController.update(recordingStatus);
trackDataHub.setRecordingTrackPaused(isPaused); trackDataHub.setRecordingStatus(recordingStatus);
}
recordingTrackPaused = isPaused;
setLockscreenPolicy(); setLockscreenPolicy();
setScreenOnPolicy(); setScreenOnPolicy();
} }
//TODO Why should this be interesting? This activity is only used for currently recording tracks.
@Override
public void onTrackRecordingId(Track.Id newTrackId) {
if (newTrackId != null && !newTrackId.equals(trackId)) {
trackId = newTrackId;
trackController.update(true, recordingTrackPaused);
trackDataHub.setRecordingTrackId(newTrackId);
}
}
} }
@@ -29,12 +29,12 @@ public class ChartPoint {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) { public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) {
if (chartByDistance) { if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits); timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
} else { } else {
timeOrDistance = trackStatistics.getTotalTime().toMillis(); timeOrDistance = trackStatistics.getTotalTime().toMillis();
} }
altitude = Distance.of(smoothedAltitude_m).to(metricUnits); altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
speed = smoothedSpeed.to(metricUnits); speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN; pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
@@ -76,4 +76,9 @@ public class ChartPoint {
public Double getPower() { public Double getPower() {
return power; return power;
} }
@Override
public String toString() {
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
}
} }
@@ -993,7 +993,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) { private double getMarkerXValue(Marker marker) {
if (chartByDistance) { if (chartByDistance) {
return marker.getLength().to(metricUnits); return marker.getLength().toKM_Miles(metricUnits);
} else { } else {
return marker.getDuration().toMillis(); return marker.getDuration().toMillis();
} }
@@ -27,9 +27,11 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns; import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -39,7 +41,9 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
/** /**
* Track data hub. * Track data hub.
@@ -71,22 +75,23 @@ public class TrackDataHub {
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints; private final int targetNumPoints;
private EGM2008Utils.EGM2008Correction egm2008Correction;
private boolean started; private boolean started;
private HandlerThread handlerThread; private HandlerThread handlerThread;
private Handler handler; private Handler handler;
private Track.Id selectedTrackId; private Track.Id selectedTrackId;
@Deprecated
//TODO Is there a better way to handle this? e.g., passing information rather from Activity than from Activity to TrackDataHub private Distance recordingDistanceInterval;
private Track.Id recordingTrackId;
@Deprecated private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
//TODO Is there a better way to handle this? e.g., passing information rather from Activity than from Activity to TrackDataHub
private boolean recordingTrackPaused;
// Track points sampling state // Track points sampling state
private int numLoadedPoints; private int numLoadedPoints;
private TrackPoint.Id firstSeenTrackPointId; private TrackPoint.Id firstSeenTrackPointId;
private TrackPoint.Id lastSeenTrackPointId; private TrackPoint.Id lastSeenTrackPointId;
private TrackStatisticsUpdater trackStatisticsUpdater;
// Registered listeners // Registered listeners
private ContentObserver tracksTableObserver; private ContentObserver tracksTableObserver;
@@ -141,12 +146,6 @@ public class TrackDataHub {
} }
}; };
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver); contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
handler.post(() -> {
if (started) {
loadDataForAll();
}
});
} }
public void stop() { public void stop() {
@@ -156,6 +155,7 @@ public class TrackDataHub {
} }
started = false; started = false;
trackStatisticsUpdater = null;
//Unregister listeners //Unregister listeners
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
@@ -208,14 +208,14 @@ public class TrackDataHub {
* Returns true if the selected track is recording. * Returns true if the selected track is recording.
*/ */
public boolean isSelectedTrackRecording() { public boolean isSelectedTrackRecording() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId); return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
} }
/** /**
* Returns true if the selected track is paused. * Returns true if the selected track is paused.
*/ */
public boolean isSelectedTrackPaused() { public boolean isSelectedTrackPaused() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId) && recordingTrackPaused; return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId()) && recordingStatus.isPaused();
} }
/** /**
@@ -351,8 +351,6 @@ public class TrackDataHub {
next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB. next = new TrackPoint.Id(localLastSeenTrackPointIdId.getId() + 1); //TODO startTrackPointId + 1 is an assumption assumption; should be derived from the DB.
} }
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
TrackPoint trackPoint = null; TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
@@ -365,6 +363,8 @@ public class TrackDataHub {
break; break;
} }
correctAltitude(trackPoint);
if (localFirstSeenTrackPointId == null) { if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId; localFirstSeenTrackPointId = trackPointId;
} }
@@ -374,7 +374,7 @@ public class TrackDataHub {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints); samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
} }
trackStatisticsUpdater.addTrackPoint(trackPoint, Distance.of(50)); //TODO trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
// Also include the last point if the selected track is not recording. // Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) { if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
@@ -408,6 +408,24 @@ public class TrackDataHub {
} }
} }
private void correctAltitude(TrackPoint trackPoint) {
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
Log.d(TAG, "No altitude correction necessary.");
return;
}
if (egm2008Correction == null || !egm2008Correction.canCorrect(trackPoint.getLocation())) {
try {
egm2008Correction = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
} catch (IOException e) {
Log.e(TAG, "Could not load altitude correction for " + trackPoint, e);
return;
}
}
trackPoint.setAltitude(Altitude.EGM2008.of(egm2008Correction.correctAltitude(trackPoint.getLocation())));
}
/** /**
* Resets the track points sampling states. * Resets the track points sampling states.
*/ */
@@ -415,13 +433,14 @@ public class TrackDataHub {
numLoadedPoints = 0; numLoadedPoints = 0;
firstSeenTrackPointId = null; firstSeenTrackPointId = null;
lastSeenTrackPointId = null; lastSeenTrackPointId = null;
trackStatisticsUpdater = new TrackStatisticsUpdater();
} }
public void setRecordingTrackId(Track.Id recordingTrackId) { public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingTrackId = recordingTrackId; this.recordingStatus = recordingStatus;
} }
public void setRecordingTrackPaused(boolean recordingTrackPaused) { public void setRecordingDistanceInterval(Distance recordingDistanceInterval) {
this.recordingTrackPaused = recordingTrackPaused; this.recordingDistanceInterval = recordingDistanceInterval;
} }
} }
@@ -0,0 +1,39 @@
package de.dennisguse.opentracks.content.data;
public abstract class Altitude {
private final double altitude_m;
private Altitude(double altitude_m) {
this.altitude_m = altitude_m;
}
public double toM() {
return altitude_m;
}
public static class WGS84 extends Altitude {
private WGS84(double altitude_m) {
super(altitude_m);
}
public static Altitude of(double altitude_m) {
return new WGS84(altitude_m);
}
}
public static class EGM2008 extends Altitude {
private EGM2008(double altitude_m) {
super(altitude_m);
}
public static Altitude of(double altitude_m) {
return new EGM2008(altitude_m);
}
}
}
@@ -98,24 +98,12 @@ public class Distance {
return toKM() * UnitConversions.KM_TO_MI; return toKM() * UnitConversions.KM_TO_MI;
} }
public double to(boolean metricUnit) { public double toKM_Miles(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES); return metricUnit ? toKM() : toMI();
} }
public double to(Unit unit) { public double toM_FT(boolean metricUnit) {
switch (unit) { return metricUnit ? toM() : toFT();
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
} }
@Override @Override
@@ -50,7 +50,7 @@ public final class Marker {
private Double latitude; private Double latitude;
private Double longitude; private Double longitude;
private Float accuracy; private Float accuracy;
private Double altitude_m; private Altitude altitude;
private Float bearing; private Float bearing;
//TODO It is the distance from the track starting point; rename to something more meaningful //TODO It is the distance from the track starting point; rename to something more meaningful
@@ -77,7 +77,7 @@ public final class Marker {
this.latitude = trackPoint.getLatitude(); this.latitude = trackPoint.getLatitude();
this.longitude = trackPoint.getLongitude(); this.longitude = trackPoint.getLongitude();
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy(); if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude(); if (trackPoint.hasAltitude()) this.altitude = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing(); if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
this.length = Distance.of(0); //TODO Not cool! this.length = Distance.of(0); //TODO Not cool!
@@ -167,7 +167,7 @@ public final class Marker {
location.setAccuracy(accuracy); location.setAccuracy(accuracy);
} }
if (hasAltitude()) { if (hasAltitude()) {
location.setAltitude(altitude_m); location.setAltitude(altitude.toM());
} }
return location; return location;
@@ -202,15 +202,15 @@ public final class Marker {
} }
public boolean hasAltitude() { public boolean hasAltitude() {
return altitude_m != null; return altitude != null;
} }
public Double getAltitude() { public Altitude getAltitude() {
return altitude_m; return altitude;
} }
public void setAltitude(double altitude_m) { public void setAltitude(Altitude altitude) {
this.altitude_m = altitude_m; this.altitude = altitude;
} }
public boolean hasBearing() { public boolean hasBearing() {
@@ -20,6 +20,7 @@ import android.os.Parcel;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@@ -51,7 +52,7 @@ public class TrackPoint {
private Double latitude; private Double latitude;
private Double longitude; private Double longitude;
private Float accuracy; private Float accuracy;
private Double altitude_m; private Altitude altitude;
private Speed speed; private Speed speed;
private Float bearing; private Float bearing;
private Distance sensorDistance_m; private Distance sensorDistance_m;
@@ -102,7 +103,7 @@ public class TrackPoint {
this.latitude = location.getLatitude(); this.latitude = location.getLatitude();
this.longitude = location.getLongitude(); this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude(); this.altitude = Altitude.WGS84.of(location.getAltitude());
this.speed = Speed.of(location.getSpeed()); this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy(); this.accuracy = location.getAccuracy();
@@ -114,11 +115,11 @@ public class TrackPoint {
this.time = time; this.time = time;
} }
public TrackPoint(double latitude, double longitude, Double altitude, Instant time) { public TrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
this(Type.TRACKPOINT); this(Type.TRACKPOINT);
this.latitude = latitude; this.latitude = latitude;
this.longitude = longitude; this.longitude = longitude;
this.altitude_m = altitude; this.altitude = altitude;
this.time = time; this.time = time;
} }
@@ -212,7 +213,7 @@ public class TrackPoint {
location.setAccuracy(accuracy); location.setAccuracy(accuracy);
} }
if (hasAltitude()) { if (hasAltitude()) {
location.setAltitude(altitude_m); location.setAltitude(altitude.toM());
} }
return location; return location;
@@ -257,15 +258,20 @@ public class TrackPoint {
public boolean hasAltitude() { public boolean hasAltitude() {
return altitude_m != null; return altitude != null;
} }
public double getAltitude() { public Altitude getAltitude() {
return altitude_m; return altitude;
} }
public void setAltitude(double altitude) { @VisibleForTesting
this.altitude_m = altitude; public void setAltitude(double altitude_m) {
this.altitude = Altitude.WGS84.of(altitude_m);
}
public void setAltitude(Altitude altitude) {
this.altitude = altitude;
} }
public boolean hasSpeed() { public boolean hasSpeed() {
@@ -39,6 +39,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns; import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -320,7 +321,7 @@ public class ContentProviderUtils {
marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6); marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
} }
if (!cursor.isNull(altitudeIndex)) { if (!cursor.isNull(altitudeIndex)) {
marker.setAltitude(cursor.getFloat(altitudeIndex)); marker.setAltitude(Altitude.WGS84.of(cursor.getFloat(altitudeIndex)));
} }
if (!cursor.isNull(accuracyIndex)) { if (!cursor.isNull(accuracyIndex)) {
marker.setAccuracy(cursor.getFloat(accuracyIndex)); marker.setAccuracy(cursor.getFloat(accuracyIndex));
@@ -489,7 +490,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6)); values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli()); values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
if (marker.hasAltitude()) { if (marker.hasAltitude()) {
values.put(MarkerColumns.ALTITUDE, marker.getAltitude()); values.put(MarkerColumns.ALTITUDE, marker.getAltitude().toM());
} }
if (marker.hasAccuracy()) { if (marker.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, marker.getAccuracy()); values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
@@ -539,7 +540,7 @@ public class ContentProviderUtils {
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex))); trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
} }
if (!cursor.isNull(indexes.altitudeIndex)) { if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex)); trackPoint.setAltitude(Altitude.WGS84.of(cursor.getFloat(indexes.altitudeIndex)));
} }
if (!cursor.isNull(indexes.accuracyIndex)) { if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex)); trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
@@ -690,7 +691,7 @@ public class ContentProviderUtils {
} }
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli()); values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude()); values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
} }
if (trackPoint.hasAccuracy()) { if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy()); values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
@@ -318,24 +318,4 @@ public class ChartFragment extends Fragment implements TrackDataListener {
void setChartView(ChartView view) { void setChartView(ChartView view) {
chartView = view; chartView = view;
} }
@VisibleForTesting
void setMetricUnits(boolean value) {
chartView.setMetricUnits(value);
}
@VisibleForTesting
void setReportSpeed(boolean value) {
chartView.setReportSpeed(value);
}
@VisibleForTesting
void setChartByDistance(boolean value) {
chartByDistance = value;
}
@VisibleForTesting
void setRecordingDistanceInterval(Distance value) {
recordingDistanceInterval = value;
}
} }
@@ -444,7 +444,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
if (preferenceShowAltitude) { if (preferenceShowAltitude) {
// Current altitude // Current altitude
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null; Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude().toM() : null;
Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits); Pair<String, String> parts = StringUtils.formatAltitude(getContext(), altitude, preferenceMetricUnits);
viewBinding.statsAltitudeCurrentValue.setText(parts.first); viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second); viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
@@ -237,7 +237,7 @@ public class GPXTrackExporter implements TrackExporter {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">"); printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
if (marker.hasAltitude()) { if (marker.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude()) + "</ele>"); printWriter.println("<ele>" + ALTITUDE_FORMAT.format(marker.getAltitude().toM()) + "</ele>");
} }
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>"); printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(marker.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
@@ -281,7 +281,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">"); printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude()) + "</ele>"); printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
} }
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>"); printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
@@ -29,8 +28,6 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
@@ -38,10 +35,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R; 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.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.Speed;
@@ -62,7 +57,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter { abstract class AbstractFileTrackImporter extends DefaultHandler implements XMLImporter.TrackParser {
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName(); private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
@@ -127,24 +122,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
} }
@Override
@NonNull
public List<Track.Id> importFile(InputStream inputStream) {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, this);
return trackIds;
} catch (IOException | SAXException | ParserConfigurationException | ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (trackIds.size() > 0) {
cleanImport();
}
throw new ImportParserException(e);
} catch (SQLiteConstraintException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportAlreadyExistsException(e);
}
}
protected void onFileEnd() { protected void onFileEnd() {
// Add markers to the last imported track // Add markers to the last imported track
int size = trackIds.size(); int size = trackIds.size();
@@ -430,7 +407,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null; return null;
} }
private TrackPoint createTrackPoint() throws ParsingException { protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
try { try {
@@ -450,68 +427,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e); throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
} }
if (altitude != null) { if (altitude != null) {
try { try {
trackPoint.setAltitude(Double.parseDouble(altitude)); trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), 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) {
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) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception 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) {
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) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
return trackPoint; return trackPoint;
} }
@@ -538,10 +461,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
} }
/** @Override
* Cleans up import. public List<Track.Id> getImportTrackIds() {
*/ return trackIds;
private void cleanImport() { }
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds); contentProviderUtils.deleteTracks(context, trackIds);
} }
@@ -571,7 +496,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
super(message); super(message);
} }
private ParsingException(@NonNull String message, Exception cause) { protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause); super(message, cause);
} }
@@ -22,7 +22,12 @@ import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -79,6 +84,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
super(context, contentProviderUtils); super(context, contentProviderUtils);
} }
@Override
public DefaultHandler getHandler() {
return this;
}
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) { switch (tag) {
@@ -199,6 +209,66 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
category = null; category = null;
} }
@Override
protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = super.createTrackPoint();
if (speed != null) {
try {
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception 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) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
if (power != null) {
try {
trackPoint.setPower(Float.parseFloat(power));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
}
}
if (gain != null) {
try {
trackPoint.setAltitudeGain(Float.parseFloat(gain));
} catch (Exception 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) {
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) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
return trackPoint;
}
/** /**
* On track point start. * On track point start.
* *
@@ -13,7 +13,6 @@ import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -47,23 +46,22 @@ public class ImportService extends JobIntentService {
} }
private void importFile(DocumentFile file) { private void importFile(DocumentFile file) {
TrackImporter trackImporter; ArrayList<Track.Id> trackIds = new ArrayList<>();
String fileExtension = FileUtils.getExtension(file); String fileExtension = FileUtils.getExtension(file);
try {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) { if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(this); trackIds.addAll(new XMLImporter(new GpxFileTrackImporter(this)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) { } else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(this); trackIds.addAll(new XMLImporter(new KmlFileTrackImporter(this)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) { } else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(this, file.getUri()); trackIds.addAll(new KmzTrackImporter().importFile(this, file.getUri()));
} else { } else {
Log.d(TAG, "Unsupported file format."); Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format)); sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return; return;
} }
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
ArrayList<Track.Id> trackIds = new ArrayList<>(trackImporter.importFile(inputStream));
if (!trackIds.isEmpty()) { if (!trackIds.isEmpty()) {
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName())); sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName()));
@@ -23,6 +23,7 @@ import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList; import java.util.ArrayList;
@@ -37,7 +38,6 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
//TODO Do not use AbstractFileTrackImporter as TrackPoint sensor generation is done differently here.
public class KmlFileTrackImporter extends AbstractFileTrackImporter { public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName(); private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
@@ -84,6 +84,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
super(context, contentProviderUtils); super(context, contentProviderUtils);
} }
@Override
public DefaultHandler getHandler() {
return this;
}
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) { switch (tag) {
@@ -22,10 +22,9 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
@@ -46,35 +45,23 @@ import de.dennisguse.opentracks.util.FileUtils;
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public class KmzTrackImporter implements TrackImporter { public class KmzTrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName(); private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png"); private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
private static final int BUFFER_SIZE = 4096; private Context context;
private final Context context; @NonNull
private final Uri uriKmzFile; public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
/**
* @param context the context
* @param uriFile URI of the kmz file.
*/
public KmzTrackImporter(Context context, Uri uriFile) {
this.context = context; this.context = context;
this.uriKmzFile = uriFile; List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
}
@Override List<Track.Id> trackIdsWithImages = new ArrayList<>();
public @NonNull
List<Track.Id> importFile(InputStream inputStream) {
List<Track.Id> trackIds = findAndParseKmlFile(inputStream);
ArrayList<Track.Id> trackIdsWithImages = new ArrayList<>();
for (Track.Id trackId : trackIds) { for (Track.Id trackId : trackIds) {
if (copyKmzImages(trackId)) { if (copyKmzImages(fileUri, trackId)) {
trackIdsWithImages.add(trackId); trackIdsWithImages.add(trackId);
deleteOrphanImages(trackId); deleteOrphanImages(trackId);
} else { } else {
@@ -90,8 +77,8 @@ public class KmzTrackImporter implements TrackImporter {
* *
* @return false if there are errors or true otherwise. * @return false if there are errors or true otherwise.
*/ */
private boolean copyKmzImages(Track.Id trackId) { private boolean copyKmzImages(Uri uri, Track.Id trackId) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile); try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry; ZipEntry zipEntry;
@@ -110,9 +97,6 @@ public class KmzTrackImporter implements TrackImporter {
} }
return true; return true;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} }
} }
@@ -154,13 +138,9 @@ public class KmzTrackImporter implements TrackImporter {
return KMZ_IMAGES_EXT.contains(fileExt); return KMZ_IMAGES_EXT.contains(fileExt);
} }
/** private List<Track.Id> findAndParseKmlFile(Uri uri) throws IOException {
* Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it. try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
* ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
* @param inputStream kmz input stream.
*/
private List<Track.Id> findAndParseKmlFile(InputStream inputStream) {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry; ZipEntry zipEntry;
ArrayList<Track.Id> trackIds = new ArrayList<>(); ArrayList<Track.Id> trackIds = new ArrayList<>();
@@ -190,9 +170,6 @@ public class KmzTrackImporter implements TrackImporter {
} catch (ImportParserException | ImportAlreadyExistsException e) { } catch (ImportParserException | ImportAlreadyExistsException e) {
Log.e(TAG, "Unable to import file", e); Log.e(TAG, "Unable to import file", e);
throw e; throw e;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportParserException(e);
} }
} }
@@ -227,32 +204,17 @@ public class KmzTrackImporter implements TrackImporter {
} }
} }
private List<Track.Id> parseKml(ZipInputStream zipInputStream) { private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context); XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlFileTrackImporter(context));
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) { InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
return kmlFileTrackImporter.importFile(byteArrayInputStream); @Override
} catch (ImportParserException | ImportAlreadyExistsException e) { public void close() {
throw e; // SAX2 always tries close InputStreams; but that would also close our ZIP file.
} catch (Exception e) {
throw new ImportParserException(e);
}
}
/**
* Gets the kml as byte array.
*
* @param zipInputStream the zip input stream
*/
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
} }
return byteArrayOutputStream.toByteArray(); };
}
return kmlFileTrackImporter.importFile(nonClosableInputStream);
} }
/** /**
@@ -262,6 +224,7 @@ public class KmzTrackImporter implements TrackImporter {
* @param trackId the track's id which image belongs to. * @param trackId the track's id which image belongs to.
* @param fileName the file name * @param fileName the file name
*/ */
@Deprecated //TODO Use JDK9's inputStream.transferTo() instead of manual buffer
private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException { private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException {
if (trackId == null || fileName.equals("")) { if (trackId == null || fileName.equals("")) {
return; return;
@@ -271,7 +234,7 @@ public class KmzTrackImporter implements TrackImporter {
File file = new File(dir, fileName); File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[4096];
int count; int count;
while ((count = zipInputStream.read(buffer)) != -1) { while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count); fileOutputStream.write(buffer, 0, count);
@@ -1,39 +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.io.file.importer;
import androidx.annotation.NonNull;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
/**
* Interface for a track importer.
*
* @author Jimmy Shih
*/
public interface TrackImporter {
/**
* Import a file.
*
* @param inputStream the file's input stream
*/
@NonNull
List<Track.Id> importFile(InputStream inputStream) throws ImportParserException;
}
@@ -0,0 +1,67 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.content.data.Track;
/**
* Uses SAX2 to parse XML files.
* <p>
* NOTE: SAX2 always closes InputStreams after processing.
*/
public class XMLImporter {
private static final String TAG = XMLImporter.class.getSimpleName();
private final TrackParser parser;
public XMLImporter(TrackParser parser) {
this.parser = parser;
}
@NonNull
public List<Track.Id> importFile(Context context, Uri uri) throws ImportParserException, ImportAlreadyExistsException, IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
return importFile(inputStream);
}
}
public List<Track.Id> importFile(InputStream inputStream) throws ImportParserException, ImportAlreadyExistsException, IOException {
try {
SAXParserFactory.newInstance().newSAXParser().parse(inputStream, parser.getHandler());
return parser.getImportTrackIds();
} catch (SAXException | ParserConfigurationException | AbstractFileTrackImporter.ParsingException e) {
Log.e(TAG, "Unable to import file", e);
if (parser.getImportTrackIds().size() > 0) {
parser.cleanImport();
}
throw new ImportParserException(e);
} catch (SQLiteConstraintException e) {
Log.e(TAG, "Unable to import file", e);
throw new ImportAlreadyExistsException(e);
}
}
interface TrackParser {
DefaultHandler getHandler();
List<Track.Id> getImportTrackIds();
void cleanImport();
}
}
@@ -33,6 +33,8 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@@ -52,7 +54,7 @@ import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.HandlerServer; import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager; import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTask;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor; import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -74,6 +76,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private static final String TAG = TrackRecordingService.class.getSimpleName(); private static final String TAG = TrackRecordingService.class.getSimpleName();
public static final RecordingStatus STATUS_DEFAULT = new RecordingStatus(null, false);
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
// The following variables are set in onCreate: // The following variables are set in onCreate:
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private PeriodicTaskExecutor voiceExecutor; private PeriodicTaskExecutor voiceExecutor;
@@ -119,16 +124,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private HandlerServer handlerServer; private HandlerServer handlerServer;
private final TrackRecordingServiceStatus serviceStatus = new TrackRecordingServiceStatus(); private RecordingStatus recordingStatus;
private MutableLiveData<RecordingStatus> recordingStatusObservable;
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
recordingStatusObservable = new MutableLiveData<>();
updateRecordingStatus(STATUS_DEFAULT);
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
handlerServer = new HandlerServer(this); handlerServer = new HandlerServer(this);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory());
notificationManager = new TrackRecordingServiceNotificationManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -149,8 +160,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override @Override
public void onDestroy() { public void onDestroy() {
serviceStatus.onStop();
handlerServer.stop(this); handlerServer.stop(this);
handlerServer = null; handlerServer = null;
@@ -181,19 +190,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// This should be the next to last operation // This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock); wakeLock = SystemUtils.releaseWakeLock(wakeLock);
recordingStatusObservable = null;
gpsStatusObservable = null;
super.onDestroy(); super.onDestroy();
} }
public boolean isRecording() { public boolean isRecording() {
return serviceStatus.isRecording(); return recordingStatus.isRecording();
} }
public boolean isPaused() { public boolean isPaused() {
return serviceStatus.getRecordingTrackPaused(); return recordingStatus.isPaused();
} }
public Track.Id getRecordingTrackId() { public Track.Id getRecordingTrackId() {
return serviceStatus.getRecordingTrackId(); return recordingStatus.getTrackId();
} }
public TrackStatistics getTrackStatistics() { public TrackStatistics getTrackStatistics() {
@@ -223,14 +235,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
} }
if (name == null) { if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(serviceStatus.getRecordingTrackId()); Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(getRecordingTrackId());
if (nextMarkerNumber == null) { if (nextMarkerNumber == null) {
nextMarkerNumber = 1; nextMarkerNumber = 1;
} }
name = getString(R.string.marker_name_format, nextMarkerNumber + 1); name = getString(R.string.marker_name_format, nextMarkerNumber + 1);
} }
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(serviceStatus.getRecordingTrackId()); TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(getRecordingTrackId());
if (trackPoint == null) { if (trackPoint == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown."); Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null; return null;
@@ -244,7 +256,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics(); TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker // Insert marker
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), stats, trackPoint, photoUrl); Marker marker = new Marker(name, description, category, icon, getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker); Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri)); return new Marker.Id(ContentUris.parseId(uri));
} }
@@ -266,7 +278,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
Track.Id trackId = new Track.Id(ContentUris.parseId(uri)); Track.Id trackId = new Track.Id(ContentUris.parseId(uri));
// Set recording status // Set recording status
serviceStatus.onChange(trackId, false); updateRecordingStatus(new RecordingStatus(trackId, false));
// Update database // Update database
track.setId(trackId); track.setId(trackId);
@@ -308,7 +320,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
insertTrackPoint(track, TrackPoint.createSegmentStartManual()); insertTrackPoint(track, TrackPoint.createSegmentStartManual());
// Set recording status // Set recording status
serviceStatus.onChange(trackId, false); updateRecordingStatus(new RecordingStatus(trackId, false));
startRecording(); startRecording();
} }
@@ -321,10 +333,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
} }
// Set recording status // Set recording status
serviceStatus.onChange(false); updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), false));
// Update database // Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId()); Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) { if (track != null) {
insertTrackPoint(track, TrackPoint.createSegmentStartManual()); insertTrackPoint(track, TrackPoint.createSegmentStartManual());
} }
@@ -373,10 +385,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// Need to remember the recordingTrackId before setting it to null // Need to remember the recordingTrackId before setting it to null
boolean wasPause = isPaused(); boolean wasPause = isPaused();
Track.Id trackId = serviceStatus.getRecordingTrackId(); Track.Id trackId = getRecordingTrackId();
// Set recording status // Set recording status
serviceStatus.onChange(null, true); updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) { if (!wasPause) {
// Update database // Update database
@@ -404,10 +416,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
} }
// Set recording status // Set recording status
serviceStatus.onChange(true); updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), true));
// Update database // Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId()); Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) { if (track != null) {
if (lastTrackPoint != null) { if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPointIfNewer(track, lastTrackPoint);
@@ -490,7 +502,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return; return;
} }
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId()); Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track == null) { if (track == null) {
Log.w(TAG, "Ignore newTrackPoint. No track."); Log.w(TAG, "Ignore newTrackPoint. No track.");
return; return;
@@ -564,17 +576,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override @Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) { public void newGpsStatus(GpsStatusValue gpsStatusValue) {
notificationManager.updateContent(getString(gpsStatusValue.message)); notificationManager.updateContent(getString(gpsStatusValue.message));
serviceStatus.onChange(gpsStatusValue);
}
public void addListener(@NonNull TrackRecordingServiceStatus.Listener listener) { //TODO This check should not be necessary, but prevents a crash; somehow the shutdown is not working correctly as we should not receive a notification then.
serviceStatus.addListener(listener); if (gpsStatusObservable != null) {
} gpsStatusObservable.postValue(gpsStatusValue);
}
//TODO Check that this is used everywhere, where addListener is called!
//Otherwise, we keep objects in referenced and waste memory until this service instance is terminated.
public void removeListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.removeListener(listener);
} }
/** /**
@@ -666,7 +672,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private void showNotification(boolean isGpsStarted) { private void showNotification(boolean isGpsStarted) {
if (isRecording()) { if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class) Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, serviceStatus.getRecordingTrackId()); .putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, getRecordingTrackId());
PendingIntent pendingIntent = TaskStackBuilder.create(this) PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackRecordingActivity.class) .addParentStack(TrackRecordingActivity.class)
.addNextIntent(intent) .addNextIntent(intent)
@@ -702,8 +708,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
this.altitudeSumManager = altitudeSumManager; this.altitudeSumManager = altitudeSumManager;
} }
public GpsStatusValue getGpsStatus() { public LiveData<GpsStatusValue> getGpsStatusObservable() {
return handlerServer.getGpsStatus(); return gpsStatusObservable;
}
public LiveData<RecordingStatus> getRecordingStatusObservable() {
return recordingStatusObservable;
}
private void updateRecordingStatus(RecordingStatus status) {
Log.i(TAG, "new status " + recordingStatus + " -> " + status);
recordingStatus = status;
recordingStatusObservable.postValue(recordingStatus);
} }
@Override @Override
@@ -717,6 +733,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
} }
} }
public class Binder extends android.os.Binder { public class Binder extends android.os.Binder {
private Binder() { private Binder() {
@@ -727,4 +744,52 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return TrackRecordingService.this; return TrackRecordingService.this;
} }
} }
public static class RecordingStatus {
private final Track.Id trackId;
private final boolean paused;
public RecordingStatus(Track.Id trackId, boolean paused) {
this.trackId = trackId;
this.paused = paused;
}
public Track.Id getTrackId() {
return trackId;
}
public boolean isRecording() {
return trackId != null;
}
public boolean isPaused() {
return paused;
}
public boolean isRecordingAndNotPaused() {
return isRecording() && !isPaused();
}
// TODO Use
// public RecordingStatus pause() {
// return new RecordingStatus(getTrackId(), true);
// }
//
// public RecordingStatus record(@NonNull Track.Id trackId) {
// return new RecordingStatus(trackId, false);
// }
public RecordingStatus stop() {
return STATUS_DEFAULT;
}
@Override
public String toString() {
return "RecordingStatus{" +
"trackId=" + trackId +
", paused=" + paused +
'}';
}
}
} }
@@ -1,83 +0,0 @@
package de.dennisguse.opentracks.services;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
/**
* This class handles the status of the recording service.
* Also offer an interface through which to know the recording Track.Id, pause status and GPS status.
*/
public class TrackRecordingServiceStatus {
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private final List<Listener> listeners = new ArrayList<>();
void addListener(@NonNull Listener listener) {
if (this.listeners.contains(listener)) {
return;
}
this.listeners.add(listener);
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
void removeListener(@NonNull Listener listener) {
this.listeners.remove(listener);
}
public boolean getRecordingTrackPaused() {
return this.recordingTrackPaused;
}
public Track.Id getRecordingTrackId() {
return this.recordingTrackId;
}
public void onStop() {
for (Listener listener : listeners) {
listener.onGpsStatus(GpsStatusValue.GPS_NONE);
listener.onTrackRecordingId(null);
listener.onTrackRecordingPaused(true);
}
listeners.clear();
}
public boolean isRecording() {
return recordingTrackId != null;
}
void onChange(Track.Id trackId, boolean paused) {
recordingTrackId = trackId;
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(boolean paused) {
recordingTrackPaused = paused;
for (Listener listener : listeners) {
listener.onTrackRecordingPaused(recordingTrackPaused);
}
}
void onChange(GpsStatusValue statusValue) {
for (Listener listener : listeners) {
listener.onGpsStatus(statusValue);
}
}
public interface Listener {
default void onGpsStatus(GpsStatusValue newValue) {}
default void onTrackRecordingPaused(boolean paused) {}
default void onTrackRecordingId(Track.Id trackId) {}
}
}
@@ -14,7 +14,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* This class handle GPS status according to received locations and some thresholds. * This class handle GPS status according to received locations` and some thresholds.
*/ */
//TODO should handle sharedpreference changes //TODO should handle sharedpreference changes
class GpsStatus { class GpsStatus {
@@ -56,6 +56,7 @@ class GpsStatus {
public void stop() { public void stop() {
stopped = true; stopped = true;
sendStatus(gpsStatus, GpsStatusValue.GPS_NONE);
} }
} }
@@ -75,10 +76,15 @@ class GpsStatus {
gpsStatusHandler = new Handler(); gpsStatusHandler = new Handler();
} }
public void start() {
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE, GpsStatusValue.GPS_ENABLED);
}
/** /**
* The client that uses GpsStatus has to call this method to stop the Runnable if needed. * The client that uses GpsStatus has to call this method to stop the Runnable if needed.
*/ */
public void stop() { public void stop() {
client.onGpsStatusChanged(gpsStatus, GpsStatusValue.GPS_NONE);
client = null; client = null;
if (gpsStatusRunner != null) { if (gpsStatusRunner != null) {
gpsStatusRunner.stop(); gpsStatusRunner.stop();
@@ -20,4 +20,8 @@ public enum GpsStatusValue {
this.icon = icon; this.icon = icon;
this.message = message; this.message = message;
} }
public boolean isGpsStarted() {
return this != GpsStatusValue.GPS_NONE && this != GpsStatusValue.GPS_DISABLED;
}
} }
@@ -63,10 +63,6 @@ public class HandlerServer {
service.newGpsStatus(gpsStatusValue); service.newGpsStatus(gpsStatusValue);
} }
public GpsStatusValue getGpsStatus() {
return locationHandler.getGpsStatus();
}
public interface HandlerServerInterface { public interface HandlerServerInterface {
void newTrackPoint(TrackPoint trackPoint, int gpsAccuracy); void newTrackPoint(TrackPoint trackPoint, int gpsAccuracy);
void newGpsStatus(GpsStatusValue gpsStatusValue); void newGpsStatus(GpsStatusValue gpsStatusValue);
@@ -38,6 +38,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
gpsStatus = new GpsStatus(context, this); gpsStatus = new GpsStatus(context, this);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
registerLocationListener(); registerLocationListener();
gpsStatus.start();
} }
@Override @Override
@@ -23,6 +23,8 @@ import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener; import android.speech.tts.UtteranceProgressListener;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
@@ -135,7 +137,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
} }
@Override @Override
public void run(TrackRecordingService trackRecordingService) { public void run(@NonNull TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) { if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null."); Log.e(TAG, "TrackRecordingService is null.");
return; return;
@@ -231,4 +233,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called. // We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
} }
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public static class Factory implements PeriodicTaskFactory {
@Override
@NonNull
public PeriodicTask create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
} }
@@ -1,31 +0,0 @@
/*
* Copyright 2010 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.services.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
@@ -16,6 +16,8 @@
package de.dennisguse.opentracks.services.tasks; package de.dennisguse.opentracks.services.tasks;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
/** /**
@@ -35,7 +37,7 @@ public interface PeriodicTask {
* *
* @param trackRecordingService the track recording service * @param trackRecordingService the track recording service
*/ */
void run(TrackRecordingService trackRecordingService); void run(@NonNull TrackRecordingService trackRecordingService);
/** /**
* Shuts down this task and clean up resources. * Shuts down this task and clean up resources.
@@ -17,6 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -52,14 +55,13 @@ public class PeriodicTaskExecutor {
private boolean metricUnits; private boolean metricUnits;
// The next distance for the distance periodic task private Distance nextTaskDistance = Distance.of(Double.MAX_VALUE);
private double nextTaskDistance = Double.MAX_VALUE;
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) { public PeriodicTaskExecutor(@NonNull TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory; this.periodicTaskFactory = periodicTaskFactory;
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getBaseContext().getResources().getString(R.string.frequency_off)); TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getString(R.string.frequency_off));
taskFrequency = TASK_FREQUENCY_OFF; taskFrequency = TASK_FREQUENCY_OFF;
} }
@@ -97,7 +99,7 @@ public class PeriodicTaskExecutor {
timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency)); timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency));
} else { } else {
// For distance periodic task // For distance periodic task
calculateNextTaskDistance(); updateNextTaskDistance();
} }
} }
@@ -128,59 +130,48 @@ public class PeriodicTaskExecutor {
return; return;
} }
Distance distance = trackStatistics.getTotalDistance(); if (trackStatistics.getTotalDistance().greaterThan(nextTaskDistance)) {
if (distance.greaterThan(Distance.of(nextTaskDistance))) {
periodicTask.run(trackRecordingService); periodicTask.run(trackRecordingService);
calculateNextTaskDistance(); updateNextTaskDistance();
} }
} }
/**
* Sets task frequency.
*
* @param taskFrequency the task frequency
*/
public void setTaskFrequency(int taskFrequency) { public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency; this.taskFrequency = taskFrequency;
restore(); restore();
} }
/**
* Sets metricUnits.
*
* @param metricUnits true to use metric units
*/
public void setMetricUnits(boolean metricUnits) { public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits; this.metricUnits = metricUnits;
calculateNextTaskDistance(); updateNextTaskDistance();
} }
/** private void updateNextTaskDistance() {
* Calculates the next distance for the distance periodic task.
*/
private void calculateNextTaskDistance() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) { if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
return; return;
} }
if (!isDistanceFrequency()) {
nextTaskDistance = Distance.of(Double.MAX_VALUE);
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics(); TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) { if (trackStatistics == null) {
return; return;
} }
if (!isDistanceFrequency()) { nextTaskDistance = calculateNextTaskDistance(trackStatistics);
nextTaskDistance = Double.MAX_VALUE; }
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
double distance = trackStatistics.getTotalDistance().to(metricUnits); @VisibleForTesting
public Distance calculateNextTaskDistance(TrackStatistics trackStatistics) {
Distance distance = trackStatistics.getTotalDistance();
// The index will be negative since the frequency is negative. Distance announcementInterval = Distance.one(metricUnits).multipliedBy(Math.abs(taskFrequency));
int index = (int) (distance / taskFrequency); int index = (int) (distance.dividedBy(announcementInterval));
index -= 1; return announcementInterval.multipliedBy(index + 1);
nextTaskDistance = taskFrequency * index;
} }
/** /**
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.services.tasks;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
/** /**
* An interface for classes that can create {@link PeriodicTask}. * An interface for classes that can create {@link PeriodicTask}.
* *
@@ -30,5 +32,6 @@ interface PeriodicTaskFactory {
* *
* @return the task, or null if the task is not supported * @return the task, or null if the task is not supported
*/ */
@NonNull
PeriodicTask create(Context context); PeriodicTask create(Context context);
} }
@@ -20,12 +20,10 @@ import java.util.Locale;
import de.dennisguse.opentracks.AbstractActivity; import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
@@ -93,7 +91,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit(); getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
} }
public static class PrefsFragment extends PreferenceFragmentCompat implements TrackRecordingServiceStatus.Listener { public static class PrefsFragment extends PreferenceFragmentCompat {
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
@@ -107,15 +105,20 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}; };
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean isRecording; private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindServiceCallback = new Runnable() { private final Runnable bindServiceCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) { if (service == null) {
service.addListener(PrefsFragment.this); Log.w(TAG, "could not get TrackRecordingService");
return;
} }
service.getRecordingStatusObservable()
.observe(PrefsFragment.this, status -> onRecordingStatusChanged(status));
} }
}; };
@@ -218,6 +221,7 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
Preference importPreference = findPreference(getString(R.string.settings_import)); Preference importPreference = findPreference(getString(R.string.settings_import));
Preference exportPreference = findPreference(getString(R.string.settings_export)); Preference exportPreference = findPreference(getString(R.string.settings_export));
boolean isRecording = recordingStatus.isRecording();
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : ""); resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
importPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : ""); importPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
exportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : ""); exportPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
@@ -292,10 +296,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}); });
} }
@Override private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
public void onTrackRecordingId(Track.Id trackId) { this.recordingStatus = status;
if (trackId != null && !isRecording && isAdded()) { if (!status.isRecording() && isAdded()) {
isRecording = true;
updatePrefsDependOnRecording(); updatePrefsDependOnRecording();
} }
} }
@@ -148,7 +148,7 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude //Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude()); updateAbsoluteAltitude(trackPoint.getAltitude().toM());
} }
if (lastTrackPoint == null || lastMovingTrackPoint == null) { if (lastTrackPoint == null || lastMovingTrackPoint == null) {
@@ -49,7 +49,7 @@ public class AnnouncementUtils {
} }
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles; int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
double distanceInUnit = distance.to(isMetricUnits); double distanceInUnit = distance.toKM_Miles(isMetricUnits);
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit); String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg; currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;
@@ -0,0 +1,176 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Objects;
import de.dennisguse.opentracks.R;
/**
* Converts WGS84 altitude to EGM2008 (should be close to height above sea level).
* <p>
* Uses <a href="https://geographiclib.sourceforge.io/">GeographicLib</a>] EGM2008 5minute undulation data.
* https://geographiclib.sourceforge.io/html/geoid.html
* <p>
* File starts at 90N, 0E (North pole) and is encoded in parallel bands as unsigned shorts.
*/
public class EGM2008Utils {
static final int EGM2008_5_DATA = R.raw.egm2008_5;
private static final int HEADER_LENGTH = 404;
private static final int RESOLUTION_IN_MINUTES = 60 / 5;
private static final int LATITUDE_CORRECTION = 360 * RESOLUTION_IN_MINUTES;
private EGM2008Utils() {
}
public static EGM2008Correction createCorrection(Context context, Location location) throws IOException {
Indices indices = getIndices(location);
try (DataInputStream dataInputStream = new DataInputStream(context.getResources().openRawResource(EGM2008_5_DATA))) {
return new EGM2008Correction(indices, dataInputStream);
}
}
@VisibleForTesting
static int getUndulationRaw(DataInputStream dataInputStream, Indices indices) throws IOException {
dataInputStream.reset();
int absoluteIndex = indices.getAbsoluteIndex();
return getUndulationRaw(dataInputStream, absoluteIndex);
}
private static int getUndulationRaw(DataInputStream dataInputStream, int undulationIndex) throws IOException {
dataInputStream.reset();
int index = HEADER_LENGTH + undulationIndex * 2; //byte size is 2
long ignored = dataInputStream.skip(index);
return dataInputStream.readUnsignedShort();
}
@VisibleForTesting
static Indices getIndices(Location location) {
double latitude = -location.getLatitude() + 90;
int latitudeIndex = (int) (latitude * RESOLUTION_IN_MINUTES);
double longitude;
if (location.getLongitude() >= 0) {
longitude = location.getLongitude();
} else {
longitude = 180 + Math.abs(location.getLongitude());
}
int longitudeIndex = (int) (longitude * RESOLUTION_IN_MINUTES);
if (longitudeIndex >= 360 * RESOLUTION_IN_MINUTES) {
longitudeIndex = 0;
}
return new Indices(latitudeIndex, longitudeIndex);
}
public static class EGM2008Correction {
protected final Indices indices;
protected final int v00;
protected final int v10;
protected final int v01;
protected final int v11;
private EGM2008Correction(Indices indices, DataInputStream dataInputStream) throws IOException {
this.indices = indices;
v00 = getUndulationRaw(dataInputStream, indices);
if (!isSouthPole()) {
v10 = getUndulationRaw(dataInputStream, indices.offset(0, 1));
v01 = getUndulationRaw(dataInputStream, indices.offset(1, 0));
v11 = getUndulationRaw(dataInputStream, indices.offset(1, 1));
} else {
v10 = 0;
v01 = 0;
v11 = 0;
}
}
public boolean canCorrect(@NonNull Location location) {
return indices.getAbsoluteIndex() == getIndices(location).getAbsoluteIndex();
}
public double correctAltitude(@NonNull Location location) {
if (!canCorrect(location))
throw new RuntimeException("Undulation data not loaded for this location.");
if (!location.hasAltitude())
throw new RuntimeException("Location has no altitude");
double undulationRaw;
if (isSouthPole()) {
// No bilinear interpolation on South Pole (not worth the time)
undulationRaw = v00;
} else {
double fLongitude = location.getLongitude() * RESOLUTION_IN_MINUTES -
(int) (location.getLongitude() * RESOLUTION_IN_MINUTES);
double fLatitude = (-location.getLatitude() + 90) * RESOLUTION_IN_MINUTES
- (int) ((-location.getLatitude() + 90) * RESOLUTION_IN_MINUTES);
// Bilinear interpolation (optimized; taken from GeopgrahicLib/Geoid.cpp)
double
a = (1 - fLongitude) * v00 + fLongitude * v01,
b = (1 - fLongitude) * v10 + fLongitude * v11;
undulationRaw = (1 - fLatitude) * a + fLatitude * b;
}
// Bilinear interpolation (not optimized)
//undulationRaw = v00 * (1 - fLongitude) * (1 - fLatitude)
// + v10 * fLongitude * (1 - fLatitude)
// + v01 * (1 - fLongitude) * fLatitude
// + v11 * fLongitude * fLatitude;
double h = 0.003 * undulationRaw - 108;
return location.getAltitude() - h;
}
private boolean isSouthPole() {
return indices.latitudeIndex == 2160;
}
}
@VisibleForTesting
static class Indices {
final int latitudeIndex;
final int longitudeIndex;
Indices(int latitudeIndex, int longitudeIndex) {
this.latitudeIndex = latitudeIndex;
this.longitudeIndex = longitudeIndex;
}
Indices offset(int latitudeOffset, int longitudeOffset) {
return new Indices(latitudeIndex + latitudeOffset, longitudeIndex + longitudeOffset);
}
int getAbsoluteIndex() {
return latitudeIndex * LATITUDE_CORRECTION + longitudeIndex;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Indices indices = (Indices) o;
return latitudeIndex == indices.latitudeIndex &&
longitudeIndex == indices.longitudeIndex;
}
@Override
public int hashCode() {
return Objects.hash(latitudeIndex, longitudeIndex);
}
}
}
+35 -22
View File
@@ -16,31 +16,28 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> android:padding="20dp">
<ImageView <LinearLayout
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="239dp"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@drawable/ic_logo_color_24dp" />
<TextView
style="@style/TextLarge"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:orientation="horizontal"
android:text="@string/app_name" android:layout_margin="8dp">
android:textSize="24sp" />
<View style="@style/StatsHorizontalLine" /> <ImageView
android:id="@+id/imageView"
android:layout_width="30dp"
android:layout_height="match_parent"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@drawable/ic_logo_color_24dp" />
<TextView <TextView
android:id="@+id/about_text_description" android:id="@+id/about_text_description"
style="@style/TextMedium" style="@style/TextMedium"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:padding="12dp" /> android:padding="12dp" />
</LinearLayout>
<View style="@style/StatsHorizontalLine" /> <View style="@style/StatsHorizontalLine" />
@@ -74,12 +71,28 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:padding="6dp"
android:linksClickable="true" android:linksClickable="true"
android:padding="6dp"
android:text="@string/about_history" /> android:text="@string/about_history" />
<View style="@style/StatsHorizontalLine" /> <View style="@style/StatsHorizontalLine" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="@string/about_third_party_data" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="@string/third_party_data" />
<View style="@style/StatsHorizontalLine" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -281,6 +281,17 @@
app:layout_constraintStart_toEndOf="@id/guideline" app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" /> app:layout_constraintTop_toBottomOf="@id/stats_altitude_horizontal_line" />
<TextView
android:id="@+id/stats_elevation_current_label_egm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:text="@string/egm2008"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/guideline2"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/stats_altitude_current_value" />
<TextView <TextView
android:id="@+id/stats_altitude_current_value" android:id="@+id/stats_altitude_current_value"
style="@style/StatsSmallValue" style="@style/StatsSmallValue"
File diff suppressed because one or more lines are too long
+1
View File
@@ -613,4 +613,5 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="track_pace_notification">Fart: %1$s</string> <string name="track_pace_notification">Fart: %1$s</string>
<string name="track_distance_notification">Afstand: %1$s</string> <string name="track_distance_notification">Afstand: %1$s</string>
<string name="track_speed_notification">Hastighed: %1$s</string> <string name="track_speed_notification">Hastighed: %1$s</string>
<string name="about_third_party_data">Data fra tredjeparter</string>
</resources> </resources>
+1
View File
@@ -610,4 +610,5 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
<string name="track_distance_notification">Kaugus: %1$s</string> <string name="track_distance_notification">Kaugus: %1$s</string>
<string name="track_speed_notification">Kiirus: %1$s</string> <string name="track_speed_notification">Kiirus: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string> <string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
</resources> </resources>
+1
View File
@@ -611,4 +611,5 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
<string name="track_distance_notification">Distantzia: %1$s</string> <string name="track_distance_notification">Distantzia: %1$s</string>
<string name="track_speed_notification">Abiadura: %1$s</string> <string name="track_speed_notification">Abiadura: %1$s</string>
<string name="track_pace_notification">Erritmoa: %1$s</string> <string name="track_pace_notification">Erritmoa: %1$s</string>
<string name="about_third_party_data">Hirugarrenen datuak</string>
</resources> </resources>
+9 -7
View File
@@ -55,13 +55,14 @@ limitations under the License.
\n \n
\nJos kaikki ei auta, ilmoita virheestä. </string> \nJos kaikki ei auta, ilmoita virheestä. </string>
<string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string> <string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string>
<string name="help_support_content"> Voit: <string name="help_support_content"> Sinä pystyt:
\n a) levittää sanaa OpenTracksista, \n \u0020a) levittää sanaa OpenTracksista,
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">ehdottaa ominaisuuksia</a>, \n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ehdottaa ominaisuuksia </a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">ilmoittaa ohjelmistovirheistä</a>, \n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ilmoita virheistä </a>,
\n d) auttaa ohjelmiston kehittämisessä, \n \u0020d) auttaa ohjelmistokehityksessä,
\n e) ostaa sovelluksen <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play -kaupasta</a>, \n \u0020e) osta <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play -kaupasta </a>,
\n f) lahjoittaa ja g) tehdä mikä mielestäsi edistäisi OpenTracksia. </string> \n \u0020f) lahjoittaa ja
\n \u0020g) tee mitä tahansa mielestäsi auttaa OpenTracksia. </string>
<string name="open_track_using_dashboard">Avaa OpenTracks-Dashboardilla</string> <string name="open_track_using_dashboard">Avaa OpenTracks-Dashboardilla</string>
<string name="open_track_as_KMZ">Avaa KMZ-muodossa</string> <string name="open_track_as_KMZ">Avaa KMZ-muodossa</string>
<string name="open_track_as_GPX">Avaa GPX-muodossa</string> <string name="open_track_as_GPX">Avaa GPX-muodossa</string>
@@ -614,4 +615,5 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
<string name="track_distance_notification">Etäisyys: %1$s</string> <string name="track_distance_notification">Etäisyys: %1$s</string>
<string name="track_pace_notification">Vauhti: %1$s</string> <string name="track_pace_notification">Vauhti: %1$s</string>
<string name="track_speed_notification">Nopeus: %1$s</string> <string name="track_speed_notification">Nopeus: %1$s</string>
<string name="about_third_party_data">Kolmannen osapuolen tiedot</string>
</resources> </resources>
+7 -7
View File
@@ -154,7 +154,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="description_sensor_heart_rate">Frequenza cardiaca (bpm)</string> <string name="description_sensor_heart_rate">Frequenza cardiaca (bpm)</string>
<string name="description_sensor_power">Potenza (W)</string> <string name="description_sensor_power">Potenza (W)</string>
<string name="description_speed_imperial">Velocità (mi/hr)</string> <string name="description_speed_imperial">Velocità (mi/hr)</string>
<string name="description_speed_metric">Velocità (km/hr)</string> <string name="description_speed_metric">Velocità (km/h)</string>
<string name="description_time">Durata</string> <string name="description_time">Durata</string>
<string name="description_total_distance">Distanza totale: %1$.2f km (%2$.1f mi)</string> <string name="description_total_distance">Distanza totale: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_time">Tempo totale: %1$s</string> <string name="description_total_time">Tempo totale: %1$s</string>
@@ -334,7 +334,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="stats_average_speed">Velocità media</string> <string name="stats_average_speed">Velocità media</string>
<string name="stats_altitude">Altitudine</string> <string name="stats_altitude">Altitudine</string>
<string name="stats_fastest_pace">Ritmo più veloce</string> <string name="stats_fastest_pace">Ritmo più veloce</string>
<string name="stats_gain">Guadagno</string> <string name="stats_gain">Salita</string>
<string name="stats_grade">Pendenza</string> <string name="stats_grade">Pendenza</string>
<string name="stats_latitude">Latitudine</string> <string name="stats_latitude">Latitudine</string>
<string name="stats_longitude">Longitude</string> <string name="stats_longitude">Longitude</string>
@@ -536,8 +536,8 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
\ng) Fare tutto ciò che si pensa possa aiutare OpenTracks. </string> \ng) Fare tutto ciò che si pensa possa aiutare OpenTracks. </string>
<string name="activity_type_escooter">Scooter elettrico</string> <string name="activity_type_escooter">Scooter elettrico</string>
<string name="app_not_installed_show_on_map">Si prega di installare un\'applicazione che possa mostrare i dati delle mappe.</string> <string name="app_not_installed_show_on_map">Si prega di installare un\'applicazione che possa mostrare i dati delle mappe.</string>
<string name="export_with_photos">Con le foto</string> <string name="export_with_photos">con foto</string>
<string name="export_without_photos">Senza foto</string> <string name="export_without_photos">senza foto</string>
<string name="export_track_already_exists_msg">Il percorso %1$s esiste già nella cartella di salvataggio.</string> <string name="export_track_already_exists_msg">Il percorso %1$s esiste già nella cartella di salvataggio.</string>
<plurals name="generic_completed_with_errors"> <plurals name="generic_completed_with_errors">
<item quantity="one">Completato con un errore</item> <item quantity="one">Completato con un errore</item>
@@ -573,9 +573,9 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="bluetooth_disabled">Per favore, attiva il Bluetooth.</string> <string name="bluetooth_disabled">Per favore, attiva il Bluetooth.</string>
<string name="settings_recording_show_on_lockscreen_while_recording_title">Mostra le statistiche sulla schermata di blocco</string> <string name="settings_recording_show_on_lockscreen_while_recording_title">Mostra le statistiche sulla schermata di blocco</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Mantieni lo schermo acceso</string> <string name="settings_recording_keep_screen_on_while_recording_title">Mantieni lo schermo acceso</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Durante la registrazione, tenere acceso lo schermo.</string> <string name="settings_recording_keep_screen_on_while_recording_summary">Durante la registrazione, tiene acceso lo schermo.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Schermo intero</string> <string name="settings_recording_fullscreen_on_while_recording_title">Schermo intero</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Durante la registrazione, espandi a schermo intero.</string> <string name="settings_recording_fullscreen_on_while_recording_summary">Durante la registrazione, espande a schermo intero.</string>
<string name="settings_night_mode_option_system">Sistema</string> <string name="settings_night_mode_option_system">Sistema</string>
<string name="settings_night_mode_option_day">Chiaro</string> <string name="settings_night_mode_option_day">Chiaro</string>
<string name="settings_night_mode_option_night">Scuro</string> <string name="settings_night_mode_option_night">Scuro</string>
@@ -607,7 +607,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="sensor_state_heart_rate_max">Frequenza cardiaca max</string> <string name="sensor_state_heart_rate_max">Frequenza cardiaca max</string>
<string name="value_pace_kilometer">%1$d:%2$02d min/cm</string> <string name="value_pace_kilometer">%1$d:%2$02d min/cm</string>
<string name="value_float_kilometer_hour">%1$.2f cp/s</string> <string name="value_float_kilometer_hour">%1$.2f cp/s</string>
<string name="stats_loss">Perdita</string> <string name="stats_loss">Discesa</string>
<string name="settings_sensor_wheel_circumference">Circonferenza della ruota (mm)</string> <string name="settings_sensor_wheel_circumference">Circonferenza della ruota (mm)</string>
<string name="description_altitude_loss">Perdita di quota: %1$d m (%2$d ft)</string> <string name="description_altitude_loss">Perdita di quota: %1$d m (%2$d ft)</string>
<string name="sensor_state_heart_rate_avg">Tasso cardiaco medio</string> <string name="sensor_state_heart_rate_avg">Tasso cardiaco medio</string>
+124 -10
View File
@@ -157,9 +157,9 @@ limitations under the License.
<string name="gps_disabled">GPS-модуль не включен. Нажмите здесь, чтобы открыть \"%1$s\". Нажмите \"Назад\", чтобы вернуться.</string> <string name="gps_disabled">GPS-модуль не включен. Нажмите здесь, чтобы открыть \"%1$s\". Нажмите \"Назад\", чтобы вернуться.</string>
<string name="gps_disabled_my_location">Не удалось определить местоположение. Добавьте другие источники передачи данных в настройках (%1$s) и убедитесь, что приему сигнала ничего не препятствует. Затем повторите попытку.</string> <string name="gps_disabled_my_location">Не удалось определить местоположение. Добавьте другие источники передачи данных в настройках (%1$s) и убедитесь, что приему сигнала ничего не препятствует. Затем повторите попытку.</string>
<string name="gps_not_found">GPS-модуль не найден</string> <string name="gps_not_found">GPS-модуль не найден</string>
<string name="gps_starting">Включение GPS-модуля…</string> <string name="gps_starting">Включение GPS-модуля</string>
<string name="gps_wait_for_better_signal">Поиск спутников GPS…</string> <string name="gps_wait_for_better_signal">Поиск спутников GPS</string>
<string name="gps_wait_for_signal">Ожидание сигнала GPS…</string> <string name="gps_wait_for_signal">Ожидание сигнала GPS</string>
<!-- Image --> <!-- Image -->
<string name="image_application">Приложение</string> <string name="image_application">Приложение</string>
<string name="image_arrow">Стрелка</string> <string name="image_arrow">Стрелка</string>
@@ -172,7 +172,7 @@ limitations under the License.
<string name="image_stop">Стоп</string> <string name="image_stop">Стоп</string>
<string name="image_track">Трек</string> <string name="image_track">Трек</string>
<!-- Import --> <!-- Import -->
<string name="import_error">Импортировано: %1$d из %2$s (из %3$s)</string> <string name="import_error">Импортировано: %1$d из %2$s (от %3$s)</string>
<string name="import_no_directory">%1$s не существует</string> <string name="import_no_directory">%1$s не существует</string>
<string name="import_no_file">Нет файлов для импорта. Подсоедините устройство к компьютеру и укажите путь к файлам, которые нужно импортировать в %1$s.</string> <string name="import_no_file">Нет файлов для импорта. Подсоедините устройство к компьютеру и укажите путь к файлам, которые нужно импортировать в %1$s.</string>
<string name="import_no_file_title">Импорт файлов</string> <string name="import_no_file_title">Импорт файлов</string>
@@ -278,7 +278,9 @@ limitations under the License.
<string name="settings_stats_units_title">Система измерений</string> <string name="settings_stats_units_title">Система измерений</string>
<!-- Share Track --> <!-- Share Track -->
<string name="share_track_share_file">Опубликовать трек</string> <string name="share_track_share_file">Опубликовать трек</string>
<string name="share_track_share_file_body">Я думаю этот маршрут покажется вам интересным.\n\nИспользуйте Google Планета Земля или другое картографическое ПО для просмотра трека.\n\n%1$s</string> <string name="share_track_share_file_body">Я думаю этот маршрут покажется вам интересным.
\n
\n%1$s</string>
<string name="share_track_subject">Хочу поделиться треком</string> <string name="share_track_subject">Хочу поделиться треком</string>
<!-- Stats --> <!-- Stats -->
<string name="stats_average_moving_pace">Средняя длина шага</string> <string name="stats_average_moving_pace">Средняя длина шага</string>
@@ -341,7 +343,7 @@ limitations under the License.
<string name="value_float_feet">%1$.2f фт</string> <string name="value_float_feet">%1$.2f фт</string>
<string name="value_float_kilometer">%1$.2f км</string> <string name="value_float_kilometer">%1$.2f км</string>
<string name="value_float_meter">%1$.2f м</string> <string name="value_float_meter">%1$.2f м</string>
<string name="value_float_mile">%1$.2f мил.</string> <string name="value_float_mile">%1$.2f мил</string>
<string name="value_float_mile_poor_gps">%1$.1f мил. (слабый сигнал)</string> <string name="value_float_mile_poor_gps">%1$.1f мил. (слабый сигнал)</string>
<string name="value_integer_feet">%1$d фт</string> <string name="value_integer_feet">%1$d фт</string>
<string name="value_integer_feet_excellent_gps">%1$d фт (отличный сигнал)</string> <string name="value_integer_feet_excellent_gps">%1$d фт (отличный сигнал)</string>
@@ -352,11 +354,11 @@ limitations under the License.
<string name="value_integer_meter_excellent_gps">%1$d м (отличный сигнал)</string> <string name="value_integer_meter_excellent_gps">%1$d м (отличный сигнал)</string>
<string name="value_integer_meter_poor_gps">%1$d м (слабый сигнал)</string> <string name="value_integer_meter_poor_gps">%1$d м (слабый сигнал)</string>
<string name="value_integer_meter_recommended">%1$d м (рекомендуется)</string> <string name="value_integer_meter_recommended">%1$d м (рекомендуется)</string>
<string name="value_integer_mile">%1$d мил.</string> <string name="value_integer_mile">%1$d мил</string>
<string name="value_integer_mile_hour">%1$d миль/ч</string> <string name="value_integer_mile_hour">%1$d миль/ч</string>
<string name="value_integer_minute">%1$d мин.</string> <string name="value_integer_minute">%1$d мин</string>
<string name="value_integer_percent">%1$d %%</string> <string name="value_integer_percent">%1$d %%</string>
<string name="value_integer_second">%1$d сек.</string> <string name="value_integer_second">%1$d сек</string>
<string name="value_none">Нет</string> <string name="value_none">Нет</string>
<string name="value_off">Выкл.</string> <string name="value_off">Выкл.</string>
<string name="value_smallest">Мин. время</string> <string name="value_smallest">Мин. время</string>
@@ -394,7 +396,7 @@ limitations under the License.
<string name="voice_speed_miles_per_hour_other">%1$.1f мили в час</string> <string name="voice_speed_miles_per_hour_other">%1$.1f мили в час</string>
<string name="voice_speed_miles_per_hour_two">2 мили в час</string> <string name="voice_speed_miles_per_hour_two">2 мили в час</string>
<string name="voice_speed_miles_per_hour_zero">0 миль в час</string> <string name="voice_speed_miles_per_hour_zero">0 миль в час</string>
<string name="voice_template">Длина треков: %1$s за %2$s (%3$s).</string> <string name="voice_template">Длина треков: %1$s за %2$s (%3$s)</string>
<string name="voice_total_distance_kilometers_few">Общая длина: %1$.2f километра</string> <string name="voice_total_distance_kilometers_few">Общая длина: %1$.2f километра</string>
<string name="voice_total_distance_kilometers_many">Общая длина: %1$.2f километров</string> <string name="voice_total_distance_kilometers_many">Общая длина: %1$.2f километров</string>
<string name="voice_total_distance_kilometers_one">Общая длина: 1 километр</string> <string name="voice_total_distance_kilometers_one">Общая длина: 1 километр</string>
@@ -486,4 +488,116 @@ limitations under the License.
<string name="help_recording_title">Как OpenTracks записывает данные\?</string> <string name="help_recording_title">Как OpenTracks записывает данные\?</string>
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> предоставляется как KMZ. </string> <string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> предоставляется как KMZ. </string>
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string> <string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string>
<string name="export_progress_summary_new_msg">Новые экспортируемые треки</string>
<string name="help_data_storage_title">Как OpenTracks записывает данные с датчиков\?</string>
<string name="export_track_already_exists_msg">Дорожка %1$s уже существует в каталоге назначения.</string>
<string name="export_without_photos">без фотографий</string>
<string name="export_with_photos">с фотографией</string>
<string name="description_default_speed_or_pace">По типу деятельности</string>
<string name="export_all_do_it_for_all">делать это во всех конфликтах</string>
<string name="stats_loss">Потери</string>
<string name="export_progress_summary_skip_msg">Пропущенные дорожки</string>
<string name="generic_click_twice_cancel">Нажмите назад, чтобы отменить</string>
<string name="generic_completed">Завершено</string>
<string name="generic_show_errors">Показать ошибки</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_skip">Скип</string>
<string name="generic_open_track">Открытая трасса</string>
<string name="export_progress_summary_errors_msg">Ошибки</string>
<string name="import_file_imported">Файл %1$s импортирован</string>
<string name="import_error_list_dialog_title">Файлы не импортированы</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="import_no_kml_file_found">Файл doc.kml не найден в KMZ</string>
<string name="import_unable_to_import_file">Невозможно импортировать файл: %1$s</string>
<string name="import_parser_error">Ошибка парсера: %1$s</string>
<string name="import_unsupported_format">Недопустимый формат файла</string>
<string name="import_thread_interrupted">Нить прервана</string>
<string name="gps_fixed_and_ready">GPS зафиксирован и готов</string>
<string name="gps_disabled_msg">GPS отключен</string>
<string name="marker_add_photo_canceled">Отменено, фотомаркер не добавлен</string>
<string name="import_progress_summary_exists_msg">Уже существует</string>
<string name="import_progress_summary_ok_msg">Импорт файлов</string>
<string name="menu_resume_track">След от резюме</string>
<string name="settings_night_mode_title">UI Тема</string>
<string name="open_track_as_trackfileformat">Открыть как %1$s</string>
<string name="open_track_as_GPX">Открыть как GPX</string>
<string name="open_track_as_KMZ">Открыть как KMZ</string>
<string name="open_track_using_dashboard">Открыть с помощью OpenTracks-Dashboard</string>
<string name="help_support_title">Как поддержать OpenTracks\?</string>
<string name="help_gps_troubleshooting_title">OpenTracks не регистрирует местоположения\?</string>
<string name="activity_type_escooter">эскулап</string>
<string name="app_not_installed_show_on_map">Пожалуйста, установите приложение, которое поддерживает представление данных карты.</string>
<string name="permission_gps_failed">OpenTracks требует разрешения на использование GPS.</string>
<string name="settings_prevent_reimport_tracks_title">Предотвращение повторного импорта треков</string>
<string name="sensor_state_cadence_max">Макс. каденция</string>
<string name="sensor_state_cadence_avg">Средний каденс</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Полный экран</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Во время записи не выключайте экран.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Не выключайте экран</string>
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показывать статистику на экране блокировки</string>
<string name="settings_default_trackfileformat">Формат экспорта/обмена файлами</string>
<string name="sensor_state_heart_rate_max">Максимальный пульс</string>
<string name="sensor_state_heart_rate_avg">Средний пульс</string>
<string name="bluetooth_disabled">Включите Bluetooth.</string>
<string name="sensor_not_known">Датчик %1$s неизвестен. Пожалуйста, проверьте настройки.</string>
<string name="sensor_state_power_avg">Средняя мощность</string>
<string name="track_recording_notification_accuracy">Точность расположения: %1$s</string>
<string name="stats_split_interval">Раздельный интервал</string>
<string name="stats_intervals">Интервалы</string>
<string name="settings_sensor_wheel_circumference">Окружность колеса (мм)</string>
<string name="settings_sensor_disconnected">Отключен от %1$s</string>
<string name="settings_sensor_connecting">Подключен к %1$s</string>
<string name="settings_night_mode_option_night">Ночь</string>
<string name="settings_night_mode_option_day">День</string>
<string name="export_progress_summary_overwrite_msg">Перезапись дорожек</string>
<string name="export_track_errors">Треки не экспортируются</string>
<string name="export_error_post_workout">Сбой экспорта после тренировки, пожалуйста, проверьте каталог экспорта.</string>
<string name="description_altitude_loss">Потеря высоты: %1$d м (%2$d футов)</string>
<string name="settings_default_export_uri_title">Каталог экспорта треков</string>
<string name="instant_export_enabled_summary">Экспорт дорожки в хранилище после завершения записи</string>
<string name="share_image_subject">Я хотел бы поделиться с вами изображением</string>
<string name="track_detail_record_success">Сейчас записываю новый трек</string>
<string name="track_detail_intervals_tab">Интервалы</string>
<string name="help_data_storage_content"> Данные датчика хранятся в каждом месте записи. Применительно к датчику пульса это означает, что для каждого места сохраняется только <i>одно</i> измерение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записаны (но они будут отображаться). </string>
<string name="help_gps_troubleshooting_content"> Что вы можете сделать:
\n a) проверить, включен ли GPS,
\n б) улучшить прием GPS (например, выехать на природу),
\n c) проверить с помощью приложения <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, и
\n г) отключите энергосбережение.
\n
\n Если все это не поможет, пожалуйста, сообщите об ошибке. </string>
<string name="track_pace_notification">Темп: %1$s</string>
<string name="track_speed_notification">Скорость: %1$s</string>
<string name="track_distance_notification">Расстояние: %1$s</string>
<string name="value_pace_kilometer">%1$d:%2$02d мин/км</string>
<string name="value_pace_mile">%1$d:%2$02d мин/миль</string>
<string name="value_float_mile_hour">%1$.2f mph</string>
<string name="value_float_kilometer_hour">%1$.2f км/ч</string>
<string name="interval_list_empty_message">Интервалов пока нет</string>
<string name="aggregated_stats_empty_message">Запишите свой первый трек, чтобы увидеть агрегированную статистику</string>
<string name="track_detail_resume_success">Теперь возобновляем путь</string>
<string name="voice_pace_lap">Время круга %1$s</string>
<string name="voice_speed_lap">Скорость круга %1$s</string>
<string name="instant_export_enabled_title">Мгновенный экспорт после тренировки</string>
<string name="toast_message_instant_track_was_exported">Последний экспортированный трек</string>
<string name="hold_to_pause">Удерживайте для паузы</string>
<string name="hold_to_stop">Удерживать для остановки</string>
<string name="share_image_body">Я думаю, вас может заинтересовать это изображение.</string>
<string name="help_support_content"> Вы можете:
\n a) распространять информацию об OpenTracks,
\n б) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">предлагать возможности</a>,
\n в) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">сообщать об ошибках</a>,
\n d) помогать в разработке программного обеспечения,
\n д) покупать в <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
\n f) жертвовать, и
\n g) делать все, что, по вашему мнению, поможет OpenTracks. </string>
<string name="settings_error_initial_values_restored">Извините, но произошла ошибка. Все настройки были возвращены к значениям по умолчанию.</string>
<string name="settings_night_mode_option_system">Система</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Во время записи присутствует полноэкранный режим.</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Во время записи показывайте статистику, не разблокируя устройство.</string>
<string name="menu_insert_gallery_img">Вставка изображения галереи</string>
<string name="sensor_could_not_scan">Не удалось выполнить сканирование устройств Bluetooth (ошибка %1$i).</string>
<string name="import_progress_summary_errors_msg">Ошибки</string>
<string name="generic_overwrite">Перезаписать</string>
<string name="import_prevent_reimport">Вы настроили предотвращение повторного импорта трека</string>
</resources> </resources>
+7
View File
@@ -17,6 +17,13 @@ limitations under the License.
<string name="app_web_url" translatable="false">https://github.com/OpenTracksApp/OpenTracks</string> <string name="app_web_url" translatable="false">https://github.com/OpenTracksApp/OpenTracks</string>
<string name="app_name" translatable="false">OpenTracks</string> <string name="app_name" translatable="false">OpenTracks</string>
<string name="egm2008" translatable="false">EGM2008</string>
<string name="third_party_data" translatable="false">
<a href="https://earth-info.nga.mil/">National Geospatial-Intelligence Agency (NGA)\'s EGM2008 model</a> 5\' minute resolution.
Data provided by <a href="https://geographiclib.sourceforge.io/html/geoid.html">GeographicLib</a>.
</string>
<!-- List here all used libraries (will be shown in the about activity --> <!-- List here all used libraries (will be shown in the about activity -->
<string name="third_party_libraries" translatable="false"> <string name="third_party_libraries" translatable="false">
<a href="https://developer.android.com/topic/libraries/support-library/">Google\'s androidX support</a> (Apache License, Version 2.0) \n <a href="https://developer.android.com/topic/libraries/support-library/">Google\'s androidX support</a> (Apache License, Version 2.0) \n
+20
View File
@@ -219,6 +219,26 @@
<item>@string/settings_recording_track_name_number_option</item> <item>@string/settings_recording_track_name_number_option</item>
</string-array> </string-array>
<string name="voice_frequency_key" translatable="false">voiceFrequency</string>
<string name="voice_frequency_default" translatable="false">@string/frequency_off</string>
<string name="frequency_off" translatable="false">0</string>
<string-array name="frequency_values">
<item>@string/frequency_off</item>
<item>1</item>
<item>2</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>30</item>
<item>60</item>
<item>-1</item>
<item>-5</item>
<item>-10</item>
<item>-25</item>
<item>-50</item>
<item>-100</item>
</string-array>
<string name="export_trackfileformat_key" translatable="false">exportTrackFileFormat</string> <string name="export_trackfileformat_key" translatable="false">exportTrackFileFormat</string>
<!-- See TrackFileFormat --> <!-- See TrackFileFormat -->
<string name="export_trackfileformat_default" translatable="false">KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES</string> <string name="export_trackfileformat_default" translatable="false">KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES</string>
@@ -18,25 +18,6 @@
<string name="split_frequency_key" translatable="false" tools:keep="@string/split_frequency_key">splitFrequency</string> <string name="split_frequency_key" translatable="false" tools:keep="@string/split_frequency_key">splitFrequency</string>
<string name="split_frequency_default" translatable="false" tools:keep="@string/split_frequency_default">@string/frequency_off</string> <string name="split_frequency_default" translatable="false" tools:keep="@string/split_frequency_default">@string/frequency_off</string>
<string name="voice_frequency_key" translatable="false">voiceFrequency</string>
<string name="voice_frequency_default" translatable="false">@string/frequency_off</string>
<string name="frequency_off" translatable="false">0</string>
<string-array name="frequency_values">
<item>@string/frequency_off</item>
<item>1</item>
<item>2</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>30</item>
<item>60</item>
<item>-1</item>
<item>-5</item>
<item>-10</item>
<item>-25</item>
<item>-50</item>
<item>-100</item>
</string-array>
<string name="chart_show_cadence_key" translatable="false" tools:keep="@string/chart_show_cadence_key">chartShowCadence</string> <string name="chart_show_cadence_key" translatable="false" tools:keep="@string/chart_show_cadence_key">chartShowCadence</string>
<bool name="chart_show_cadence_default" translatable="false" tools:keep="@bool/chart_show_cadence_default"> <bool name="chart_show_cadence_default" translatable="false" tools:keep="@bool/chart_show_cadence_default">
+1
View File
@@ -20,6 +20,7 @@ limitations under the License.
<string name="about_version_name">Version name: %1$s</string> <string name="about_version_name">Version name: %1$s</string>
<string name="about_version_code">Version code: %1$d</string> <string name="about_version_code">Version code: %1$d</string>
<string name="about_history">OpenTracks is a fork of <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (discontinued in 2016).</string> <string name="about_history">OpenTracks is a fork of <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (discontinued in 2016).</string>
<string name="about_third_party_data">Third-party data</string>
<string name="about_libraries">Libraries</string> <string name="about_libraries">Libraries</string>
<!-- Help --> <!-- Help -->
<string name="help_feature_title">OpenTracks has the following features:</string> <string name="help_feature_title">OpenTracks has the following features:</string>