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
strategy:
matrix:
api-level: [29]
api-level: [ 23, 26, 29 ]
target: [default]
steps:
- name: checkout
+2
View File
@@ -28,6 +28,8 @@ However, often only one value is provided.
* Wahoo Speed (Model: WFRPMSPD)
* CooSpo cadence and speed sensor (model: BK467)
### Cadence
* Garmin Cadence Sensor 2
+11 -7
View File
@@ -3,17 +3,17 @@ apply plugin: 'com.android.application'
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:4.2.0'
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
@@ -49,10 +49,14 @@ android {
viewBinding = true
}
aaptOptions {
cruncherEnabled = false
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4009
versionName "v3.17.3"
versionCode 4046
versionName "v3.18.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,7 +90,7 @@ android {
}
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'
implementation 'androidx.appcompat:appcompat:1.2.0'
@@ -99,5 +103,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.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
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
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+104 -103
View File
@@ -1,103 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@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%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@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%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -54,7 +54,7 @@ public class ChartPointTest {
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
// then
assertEquals(0.05, point.getAltitude(), 0.01);
assertEquals(50, point.getAltitude(), 0.01);
assertEquals(36, point.getSpeed(), 0.01);
assertEquals(1.66, point.getPace(), 0.01);
}
@@ -28,7 +28,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.Distance;
/**
* Tests {@link ChartFragment}.
@@ -59,7 +58,6 @@ public class ChartFragmentTest {
boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
}
@Test
@@ -3,24 +3,26 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Looper;
import android.util.Log;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
@@ -30,6 +32,7 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
@@ -41,7 +44,6 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
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.
* <p>
*/
@RunWith(JUnit4.class)
@RunWith(AndroidJUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
@@ -71,7 +73,6 @@ public class ExportImportTest {
}
private final Context context = ApplicationProvider.getApplicationContext();
private final SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(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_DESCRIPTION = "the description";
private File tmpFile;
private Uri tmpFileUri;
private Track track;
private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
@@ -86,6 +90,18 @@ public class ExportImportTest {
private Track.Id trackId;
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 {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
@@ -138,7 +154,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void kml_with_trackdetail() throws TimeoutException {
public void kml_with_trackdetail() throws TimeoutException, IOException {
setUp(false);
// given
@@ -148,13 +164,12 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
@@ -179,7 +194,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() throws TimeoutException {
public void kml_with_trackdetail_and_sensordata() throws TimeoutException, IOException {
setUp(true);
// given
@@ -189,13 +204,12 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
@@ -217,9 +231,48 @@ public class ExportImportTest {
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
@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);
// given
@@ -231,13 +284,11 @@ public class ExportImportTest {
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context);
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new KmlFileTrackImporter(context));
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
@@ -247,7 +298,7 @@ public class ExportImportTest {
@LargeTest
@Test
public void gpx() throws TimeoutException {
public void gpx() throws TimeoutException, IOException {
setUp(true);
// given
@@ -257,13 +308,12 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
@@ -295,7 +345,7 @@ public class ExportImportTest {
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
public void gpx_duplicate_trackUUID() throws TimeoutException {
public void gpx_duplicate_trackUUID() throws TimeoutException, IOException {
setUp(false);
// given
@@ -308,12 +358,11 @@ public class ExportImportTest {
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(track, outputStream);
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
XMLImporter trackImporter = new XMLImporter(new GpxFileTrackImporter(context, contentProviderUtils));
importTrackId = trackImporter.importFile(inputStream).get(0);
// then
@@ -368,7 +417,7 @@ public class ExportImportTest {
}
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.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)) {
@@ -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) {
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.setSpeed(Speed.of(speed));
tp.setHeartRate_bpm(heartRate);
@@ -11,6 +11,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@@ -48,9 +49,9 @@ public class LegacyImportTest {
*/
@LargeTest
@Test
public void kml_with_statistics_marker() {
public void kml_with_statistics_marker() throws IOException {
// 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);
// when
@@ -90,9 +91,9 @@ public class LegacyImportTest {
*/
@LargeTest
@Test(expected = ImportParserException.class)
public void kml_without_locations() {
public void kml_without_locations() throws IOException {
// 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);
// when
@@ -104,9 +105,9 @@ public class LegacyImportTest {
*/
@LargeTest
@Test
public void gpx_with_pause_resume() {
public void gpx_with_pause_resume() throws IOException {
// 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);
// when
@@ -151,7 +152,7 @@ public class LegacyImportTest {
if (altitude == null) {
assertFalse(trackPoint.hasAltitude());
} 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 org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -83,6 +84,10 @@ public class TrackRecordingServiceTest {
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 SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -13,6 +13,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
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.
*/
@RunWith(AndroidJUnit4.class)
//TODO Implement as mock test; no need to store data in database
public class TrackRecordingServiceTestLocation {
@Rule
@@ -63,6 +65,11 @@ public class TrackRecordingServiceTestLocation {
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() throws TimeoutException {
// 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.Instant;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -53,8 +54,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -71,8 +72,8 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
// when
@@ -90,11 +91,11 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
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));
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));
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));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
@@ -122,9 +123,9 @@ public class TrackStatisticsUpdaterTest {
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
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 androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -36,6 +38,8 @@ import de.dennisguse.opentracks.util.IntentUtils;
*
* @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 {
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;
private static final int DELETE_REQUEST_CODE = 3;
protected Track.Id recordingTrackId;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
@@ -68,7 +70,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
boolean stopRecording = false;
for (Track.Id trackId : trackIds) {
if (trackId.equals(recordingTrackId)) {
if (trackId.equals(getRecordingTrackId())) {
stopRecording = true;
break;
}
@@ -87,10 +89,17 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
* Gets the track recording service connection.
* 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.
*/
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.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
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.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -51,7 +51,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
*
* @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";
@@ -59,8 +59,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private ContentProviderUtils contentProviderUtils;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter;
@@ -74,9 +74,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(MarkerListActivity.this);
if (service == null) {
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
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);
}
@@ -290,6 +294,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
// Do nothing
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
@@ -344,14 +352,4 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
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.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
@@ -37,6 +38,8 @@ import de.dennisguse.opentracks.util.StringUtils;
*
* @author Jimmy Shih
*/
//TODO Subscribe to TrackRecordingService.recordingStatus
//TODO Could be a fragment
public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName();
@@ -54,17 +57,16 @@ public class TrackController implements View.OnTouchListener {
private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition;
private boolean isRecording;
private boolean isRecordingPaused;
private TrackRecordingService.RecordingStatus recordingStatus;
private Duration totalTime;
private boolean isResumed = false;
private final Callback callback;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed && isRecording && !isRecordingPaused) {
if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
@@ -84,7 +86,7 @@ public class TrackController implements View.OnTouchListener {
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isRecordingPaused)) {
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return;
}
@@ -98,6 +100,10 @@ public class TrackController implements View.OnTouchListener {
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (!isResumed()) {
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
return false;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
@@ -120,7 +126,7 @@ public class TrackController implements View.OnTouchListener {
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isRecordingPaused) {
&& recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration);
@@ -143,7 +149,7 @@ public class TrackController implements View.OnTouchListener {
//To stop a recording
if (viewBinding.trackControllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording) {
&& recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration);
@@ -165,13 +171,15 @@ public class TrackController implements View.OnTouchListener {
return false;
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
public void update(TrackRecordingService.RecordingStatus recordingStatus) {
if (!isResumed()) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
isRecording = recording;
isRecordingPaused = paused;
this.recordingStatus = recordingStatus;
boolean isRecording = recordingStatus.isRecording();
boolean isRecordingPaused = recordingStatus.isPaused();
boolean visible = alwaysShow || isRecording;
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);
if (isRecording) {
if (recordingStatus.isRecording()) {
updateTotalTime();
setTotalTime();
}
}
void onResume(boolean recording, boolean paused) {
isResumed = true;
update(recording, paused);
void onResume(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
update(recordingStatus);
}
void onPause() {
isResumed = false;
recordingStatus = null;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
@@ -235,12 +243,18 @@ public class TrackController implements View.OnTouchListener {
}
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 {
void recordStart();
void recordPause();
void recordStop();
}
}
@@ -38,6 +38,7 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
@@ -55,7 +56,6 @@ import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -71,7 +71,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @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();
@@ -80,7 +80,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController;
private ResourceCursorAdapter resourceCursorAdapter;
private GpsStatusValue gpsStatusValue;
private TrackListBinding viewBinding;
@@ -89,7 +88,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Preferences
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
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -118,7 +118,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
trackController.update(isRecording(), recordingTrackPaused);
trackController.onResume(recordingStatus);
});
}
}
@@ -133,27 +133,29 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public void run() {
// 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();
if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording");
gpsStatusValue = GpsStatusValue.GPS_NONE;
Log.e(TAG, "service not available to start gps or a new recording");
return;
}
service.getRecordingStatusObservable()
.observe(TrackListActivity.this, status -> onRecordingStatusChanged(status));
// Get GPS status and listen GPS status changes.
gpsStatusValue = service.getGpsStatus();
updateGpsMenuItem(true, isRecording());
service.addListener(TrackListActivity.this);
service.getGpsStatusObservable()
.observe(TrackListActivity.this, status -> onGpsStatusChanged(status));
if (isGpsStarted()) {
updateGpsMenuItem(true, recordingStatus.isRecording());
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
return;
}
//TODO Not cool to do this in a callback that might be called more than once!
service.tryStartGps();
gpsStatusValue = GpsStatusValue.GPS_ENABLED;
updateGpsMenuItem(true, isRecording());
}
};
@@ -165,8 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
gpsStatusValue = GpsStatusValue.GPS_NONE;
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
@@ -181,7 +181,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
if (isRecording() && trackId == recordingTrackId.getId()) {
if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.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);
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);
int iconId = TrackIconUtils.getIconDrawable(icon);
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 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,
startTime, true, category, description, false);
}
@@ -265,7 +265,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
trackController.onResume(isRecording(), recordingTrackPaused);
trackController.onResume(recordingStatus);
}
@Override
@@ -306,7 +306,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(isGpsStarted(), isRecording());
updateMenuItems(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false);
@@ -322,7 +322,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!isGpsStarted()) {
if (!gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
@@ -413,8 +413,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Do nothing
}
private boolean isGpsStarted() {
return gpsStatusValue != GpsStatusValue.GPS_NONE && gpsStatusValue != GpsStatusValue.GPS_DISABLED;
@Nullable
@Override
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
}
private void requestGPSPermissions() {
@@ -554,16 +556,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public void recordStart() {
if (recordingTrackId == null) {
if (recordingStatus.getTrackId() == null) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
} else if (recordingStatus.isPaused()) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
}
}
@@ -571,7 +572,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void recordPause() {
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
@@ -580,29 +580,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
}
private boolean isRecording() {
return recordingTrackId != null;
}
@Override
public void onGpsStatus(GpsStatusValue newStatus) {
public void onGpsStatusChanged(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
public void onTrackRecordingId(Track.Id trackId) {
if (!isRecording() && trackId != null) {
trackController.update(true, recordingTrackPaused);
}
recordingTrackId = trackId;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackController.update(recordingStatus);
}
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
@@ -24,6 +25,7 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
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.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
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.
@@ -54,7 +56,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
* @author Rodrigo Damazio
*/
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface, TrackRecordingServiceStatus.Listener {
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
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.
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub;
private TrackRecordedBinding viewBinding;
private Track.Id trackId;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -79,8 +83,22 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(TrackRecordedActivity.this);
if (service == null) {
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);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
handleIntent(getIntent());
trackDataHub = new TrackDataHub(this);
@@ -110,6 +130,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
trackDataHub.start();
}
@@ -132,6 +156,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onStop();
trackRecordingServiceConnection.unbind(this);
trackDataHub.stop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -162,7 +187,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(recordingTrackId == null);
menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu);
@@ -217,12 +242,13 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return super.onOptionsItemSelected(item);
}
@Nullable
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
// Not needed.
return null;
protected Track.Id getRecordingTrackId() {
return recordingStatus.getTrackId();
}
@Override
protected void onTrackDeleted() {
runOnUiThread(this::finish);
@@ -305,8 +331,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
startPostponedEnterTransition();
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
}
}
@@ -28,7 +28,6 @@ import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
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.
//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";
@@ -62,20 +61,23 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private TrackRecordingBinding viewBinding;
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
@Deprecated
private Track.Id trackId;
private boolean recordingTrackPaused;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (is available), update the total time in trackController.
runOnUiThread(() -> trackController.update(true, recordingTrackPaused));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(TrackRecordingActivity.this, status -> onRecordingStatusChanged(status));
if (!service.isRecording()) {
if (trackId == null) {
// trackId isn't initialized -> leads a new recording.
@@ -87,12 +89,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on.
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
trackController.onResume(recordingStatus);
}
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)) {
setFullscreenPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.recording_distance_interval_key, key)) {
trackDataHub.setRecordingDistanceInterval(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, TrackRecordingActivity.this));
}
if (key == null) return;
runOnUiThread(() -> {
TrackRecordingActivity.this.invalidateOptionsMenu();
trackController.update(true, recordingTrackPaused);
});
runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu);
}
};
@@ -207,13 +208,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
super.onResume();
// Update UI
this.invalidateOptionsMenu();
invalidateOptionsMenu();
if (trackId != null) {
//TODO Pass recordingStatus directly to them
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingTrackId(trackId);
trackDataHub.setRecordingTrackPaused(recordingTrackPaused);
trackController.onResume(true, recordingTrackPaused);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
}
/*
@@ -264,7 +265,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingTrackPaused);
updateMenuItems(recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu);
}
@@ -339,14 +340,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
public void recordStart() {
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
}
@Override
public void recordPause() {
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
@Override
@@ -404,24 +403,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(true, isPaused);
trackDataHub.setRecordingTrackPaused(isPaused);
}
recordingTrackPaused = isPaused;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackController.update(recordingStatus);
trackDataHub.setRecordingStatus(recordingStatus);
setLockscreenPolicy();
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) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits);
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
altitude = Distance.of(smoothedAltitude_m).to(metricUnits);
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
@@ -76,4 +76,9 @@ public class ChartPoint {
public Double getPower() {
return power;
}
@Override
public String toString() {
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
}
}
@@ -993,7 +993,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
return marker.getLength().to(metricUnits);
return marker.getLength().toKM_Miles(metricUnits);
} else {
return marker.getDuration().toMillis();
}
@@ -27,9 +27,11 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -39,7 +41,9 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
/**
* Track data hub.
@@ -71,22 +75,23 @@ public class TrackDataHub {
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private EGM2008Utils.EGM2008Correction egm2008Correction;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
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 Track.Id recordingTrackId;
@Deprecated
//TODO Is there a better way to handle this? e.g., passing information rather from Activity than from Activity to TrackDataHub
private boolean recordingTrackPaused;
private Distance recordingDistanceInterval;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
// Track points sampling state
private int numLoadedPoints;
private TrackPoint.Id firstSeenTrackPointId;
private TrackPoint.Id lastSeenTrackPointId;
private TrackStatisticsUpdater trackStatisticsUpdater;
// Registered listeners
private ContentObserver tracksTableObserver;
@@ -141,12 +146,6 @@ public class TrackDataHub {
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
handler.post(() -> {
if (started) {
loadDataForAll();
}
});
}
public void stop() {
@@ -156,6 +155,7 @@ public class TrackDataHub {
}
started = false;
trackStatisticsUpdater = null;
//Unregister listeners
ContentResolver contentResolver = context.getContentResolver();
@@ -208,14 +208,14 @@ public class TrackDataHub {
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId != null && selectedTrackId.equals(recordingTrackId);
return selectedTrackId != null && selectedTrackId.equals(recordingStatus.getTrackId());
}
/**
* Returns true if the selected track is paused.
*/
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.
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
TrackPoint trackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, next)) {
@@ -365,6 +363,8 @@ public class TrackDataHub {
break;
}
correctAltitude(trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
}
@@ -374,7 +374,7 @@ public class TrackDataHub {
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.
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.
*/
@@ -415,13 +433,14 @@ public class TrackDataHub {
numLoadedPoints = 0;
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
trackStatisticsUpdater = new TrackStatisticsUpdater();
}
public void setRecordingTrackId(Track.Id recordingTrackId) {
this.recordingTrackId = recordingTrackId;
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
}
public void setRecordingTrackPaused(boolean recordingTrackPaused) {
this.recordingTrackPaused = recordingTrackPaused;
public void setRecordingDistanceInterval(Distance recordingDistanceInterval) {
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;
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES);
public double toKM_Miles(boolean metricUnit) {
return metricUnit ? toKM() : toMI();
}
public double to(Unit unit) {
switch (unit) {
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
public double toM_FT(boolean metricUnit) {
return metricUnit ? toM() : toFT();
}
@Override
@@ -50,7 +50,7 @@ public final class Marker {
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Altitude altitude;
private Float bearing;
//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.longitude = trackPoint.getLongitude();
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();
this.length = Distance.of(0); //TODO Not cool!
@@ -167,7 +167,7 @@ public final class Marker {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
location.setAltitude(altitude.toM());
}
return location;
@@ -202,15 +202,15 @@ public final class Marker {
}
public boolean hasAltitude() {
return altitude_m != null;
return altitude != null;
}
public Double getAltitude() {
return altitude_m;
public Altitude getAltitude() {
return altitude;
}
public void setAltitude(double altitude_m) {
this.altitude_m = altitude_m;
public void setAltitude(Altitude altitude) {
this.altitude = altitude;
}
public boolean hasBearing() {
@@ -20,6 +20,7 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
@@ -51,7 +52,7 @@ public class TrackPoint {
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Altitude altitude;
private Speed speed;
private Float bearing;
private Distance sensorDistance_m;
@@ -102,7 +103,7 @@ public class TrackPoint {
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.altitude = Altitude.WGS84.of(location.getAltitude());
this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy();
@@ -114,11 +115,11 @@ public class TrackPoint {
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.latitude = latitude;
this.longitude = longitude;
this.altitude_m = altitude;
this.altitude = altitude;
this.time = time;
}
@@ -212,7 +213,7 @@ public class TrackPoint {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
location.setAltitude(altitude.toM());
}
return location;
@@ -257,15 +258,20 @@ public class TrackPoint {
public boolean hasAltitude() {
return altitude_m != null;
return altitude != null;
}
public double getAltitude() {
return altitude_m;
public Altitude getAltitude() {
return altitude;
}
public void setAltitude(double altitude) {
this.altitude_m = altitude;
@VisibleForTesting
public void setAltitude(double altitude_m) {
this.altitude = Altitude.WGS84.of(altitude_m);
}
public void setAltitude(Altitude altitude) {
this.altitude = altitude;
}
public boolean hasSpeed() {
@@ -39,6 +39,7 @@ import java.util.List;
import java.util.UUID;
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.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
@@ -320,7 +321,7 @@ public class ContentProviderUtils {
marker.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(altitudeIndex)) {
marker.setAltitude(cursor.getFloat(altitudeIndex));
marker.setAltitude(Altitude.WGS84.of(cursor.getFloat(altitudeIndex)));
}
if (!cursor.isNull(accuracyIndex)) {
marker.setAccuracy(cursor.getFloat(accuracyIndex));
@@ -489,7 +490,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.LATITUDE, (int) (marker.getLatitude() * 1E6));
values.put(MarkerColumns.TIME, marker.getTime().toEpochMilli());
if (marker.hasAltitude()) {
values.put(MarkerColumns.ALTITUDE, marker.getAltitude());
values.put(MarkerColumns.ALTITUDE, marker.getAltitude().toM());
}
if (marker.hasAccuracy()) {
values.put(MarkerColumns.ACCURACY, marker.getAccuracy());
@@ -539,7 +540,7 @@ public class ContentProviderUtils {
trackPoint.setTime(Instant.ofEpochMilli(cursor.getLong(indexes.timeIndex)));
}
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)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
@@ -690,7 +691,7 @@ public class ContentProviderUtils {
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
}
if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
@@ -318,24 +318,4 @@ public class ChartFragment extends Fragment implements TrackDataListener {
void setChartView(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) {
// 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);
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
@@ -237,7 +237,7 @@ public class GPXTrackExporter implements TrackExporter {
if (printWriter != null) {
printWriter.println("<wpt " + formatLocation(marker.getLatitude(), marker.getLongitude()) + ">");
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("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
@@ -281,7 +281,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
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>");
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
@@ -29,8 +28,6 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -38,10 +35,8 @@ import java.util.List;
import java.util.Locale;
import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
@@ -62,7 +57,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @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();
@@ -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() {
// Add markers to the last imported track
int size = trackIds.size();
@@ -430,7 +407,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null;
}
private TrackPoint createTrackPoint() throws ParsingException {
protected TrackPoint createTrackPoint() throws ParsingException {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
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);
}
if (altitude != null) {
try {
trackPoint.setAltitude(Double.parseDouble(altitude));
trackPoint.setAltitude(Altitude.WGS84.of(Double.parseDouble(altitude)));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
if (speed != null) {
try {
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) {
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;
}
@@ -538,10 +461,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
/**
* Cleans up import.
*/
private void cleanImport() {
@Override
public List<Track.Id> getImportTrackIds() {
return trackIds;
}
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
@@ -571,7 +496,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
super(message);
}
private ParsingException(@NonNull String message, Exception cause) {
protected ParsingException(@NonNull String message, Exception cause) {
super(message, cause);
}
@@ -22,7 +22,12 @@ import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes;
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.provider.ContentProviderUtils;
@@ -79,6 +84,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
super(context, contentProviderUtils);
}
@Override
public DefaultHandler getHandler() {
return this;
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
@@ -199,6 +209,66 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
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.
*
@@ -13,7 +13,6 @@ import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
@@ -47,23 +46,22 @@ public class ImportService extends JobIntentService {
}
private void importFile(DocumentFile file) {
TrackImporter trackImporter;
ArrayList<Track.Id> trackIds = new ArrayList<>();
String fileExtension = FileUtils.getExtension(file);
try {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(this);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(this);
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(this, file.getUri());
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
ArrayList<Track.Id> trackIds = new ArrayList<>(trackImporter.importFile(inputStream));
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new GpxFileTrackImporter(this)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackIds.addAll(new XMLImporter(new KmlFileTrackImporter(this)).importFile(this, file.getUri()));
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackIds.addAll(new KmzTrackImporter().importFile(this, file.getUri()));
} else {
Log.d(TAG, "Unsupported file format.");
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
return;
}
if (!trackIds.isEmpty()) {
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.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.ArrayList;
@@ -37,7 +38,6 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
*
* @author Jimmy Shih
*/
//TODO Do not use AbstractFileTrackImporter as TrackPoint sensor generation is done differently here.
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
@@ -84,6 +84,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
super(context, contentProviderUtils);
}
@Override
public DefaultHandler getHandler() {
return this;
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
@@ -22,10 +22,9 @@ import android.util.Log;
import androidx.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -46,35 +45,23 @@ import de.dennisguse.opentracks.util.FileUtils;
*
* @author Jimmy Shih
*/
public class KmzTrackImporter implements TrackImporter {
public class KmzTrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
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;
private final Uri uriKmzFile;
/**
* @param context the context
* @param uriFile URI of the kmz file.
*/
public KmzTrackImporter(Context context, Uri uriFile) {
@NonNull
public List<Track.Id> importFile(Context context, Uri fileUri) throws IOException {
this.context = context;
this.uriKmzFile = uriFile;
}
List<Track.Id> trackIds = findAndParseKmlFile(fileUri);
@Override
public @NonNull
List<Track.Id> importFile(InputStream inputStream) {
List<Track.Id> trackIds = findAndParseKmlFile(inputStream);
ArrayList<Track.Id> trackIdsWithImages = new ArrayList<>();
List<Track.Id> trackIdsWithImages = new ArrayList<>();
for (Track.Id trackId : trackIds) {
if (copyKmzImages(trackId)) {
if (copyKmzImages(fileUri, trackId)) {
trackIdsWithImages.add(trackId);
deleteOrphanImages(trackId);
} else {
@@ -90,8 +77,8 @@ public class KmzTrackImporter implements TrackImporter {
*
* @return false if there are errors or true otherwise.
*/
private boolean copyKmzImages(Track.Id trackId) {
try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile);
private boolean copyKmzImages(Uri uri, Track.Id trackId) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
@@ -110,9 +97,6 @@ public class KmzTrackImporter implements TrackImporter {
}
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);
}
/**
* Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it.
*
* @param inputStream kmz input stream.
*/
private List<Track.Id> findAndParseKmlFile(InputStream inputStream) {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
private List<Track.Id> findAndParseKmlFile(Uri uri) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
ArrayList<Track.Id> trackIds = new ArrayList<>();
@@ -190,9 +170,6 @@ public class KmzTrackImporter implements TrackImporter {
} catch (ImportParserException | ImportAlreadyExistsException e) {
Log.e(TAG, "Unable to import file", 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) {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
private List<Track.Id> parseKml(ZipInputStream zipInputStream) throws IOException {
XMLImporter kmlFileTrackImporter = new XMLImporter(new KmlFileTrackImporter(context));
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} catch (ImportParserException | ImportAlreadyExistsException e) {
throw e;
} 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);
InputStream nonClosableInputStream = new FilterInputStream(zipInputStream) {
@Override
public void close() {
// SAX2 always tries close InputStreams; but that would also close our ZIP file.
}
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 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 {
if (trackId == null || fileName.equals("")) {
return;
@@ -271,7 +234,7 @@ public class KmzTrackImporter implements TrackImporter {
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
byte[] buffer = new byte[4096];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
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.VisibleForTesting;
import androidx.core.app.TaskStackBuilder;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.time.Duration;
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.sensors.AltitudeSumManager;
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.settings.SettingsActivity;
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();
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:
private ContentProviderUtils contentProviderUtils;
private PeriodicTaskExecutor voiceExecutor;
@@ -119,16 +124,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private HandlerServer handlerServer;
private final TrackRecordingServiceStatus serviceStatus = new TrackRecordingServiceStatus();
private RecordingStatus recordingStatus;
private MutableLiveData<RecordingStatus> recordingStatusObservable;
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
@Override
public void onCreate() {
super.onCreate();
recordingStatusObservable = new MutableLiveData<>();
updateRecordingStatus(STATUS_DEFAULT);
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
handlerServer = new HandlerServer(this);
contentProviderUtils = new ContentProviderUtils(this);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTask.Factory());
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -149,8 +160,6 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void onDestroy() {
serviceStatus.onStop();
handlerServer.stop(this);
handlerServer = null;
@@ -181,19 +190,22 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
recordingStatusObservable = null;
gpsStatusObservable = null;
super.onDestroy();
}
public boolean isRecording() {
return serviceStatus.isRecording();
return recordingStatus.isRecording();
}
public boolean isPaused() {
return serviceStatus.getRecordingTrackPaused();
return recordingStatus.isPaused();
}
public Track.Id getRecordingTrackId() {
return serviceStatus.getRecordingTrackId();
return recordingStatus.getTrackId();
}
public TrackStatistics getTrackStatistics() {
@@ -223,14 +235,14 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
if (name == null) {
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(serviceStatus.getRecordingTrackId());
Integer nextMarkerNumber = contentProviderUtils.getNextMarkerNumber(getRecordingTrackId());
if (nextMarkerNumber == null) {
nextMarkerNumber = 1;
}
name = getString(R.string.marker_name_format, nextMarkerNumber + 1);
}
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(serviceStatus.getRecordingTrackId());
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(getRecordingTrackId());
if (trackPoint == null) {
Log.i(TAG, "Could not create a marker as trackPoint is unknown.");
return null;
@@ -244,7 +256,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// 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);
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));
// Set recording status
serviceStatus.onChange(trackId, false);
updateRecordingStatus(new RecordingStatus(trackId, false));
// Update database
track.setId(trackId);
@@ -308,7 +320,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
insertTrackPoint(track, TrackPoint.createSegmentStartManual());
// Set recording status
serviceStatus.onChange(trackId, false);
updateRecordingStatus(new RecordingStatus(trackId, false));
startRecording();
}
@@ -321,10 +333,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
// Set recording status
serviceStatus.onChange(false);
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), false));
// Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
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
boolean wasPause = isPaused();
Track.Id trackId = serviceStatus.getRecordingTrackId();
Track.Id trackId = getRecordingTrackId();
// Set recording status
serviceStatus.onChange(null, true);
updateRecordingStatus(STATUS_DEFAULT);
if (!wasPause) {
// Update database
@@ -404,10 +416,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
// Set recording status
serviceStatus.onChange(true);
updateRecordingStatus(new RecordingStatus(getRecordingTrackId(), true));
// Update database
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track != null) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
@@ -490,7 +502,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
Track track = contentProviderUtils.getTrack(serviceStatus.getRecordingTrackId());
Track track = contentProviderUtils.getTrack(getRecordingTrackId());
if (track == null) {
Log.w(TAG, "Ignore newTrackPoint. No track.");
return;
@@ -564,17 +576,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
notificationManager.updateContent(getString(gpsStatusValue.message));
serviceStatus.onChange(gpsStatusValue);
}
public void addListener(@NonNull TrackRecordingServiceStatus.Listener listener) {
serviceStatus.addListener(listener);
}
//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);
//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.
if (gpsStatusObservable != null) {
gpsStatusObservable.postValue(gpsStatusValue);
}
}
/**
@@ -666,7 +672,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
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)
.addParentStack(TrackRecordingActivity.class)
.addNextIntent(intent)
@@ -702,8 +708,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
this.altitudeSumManager = altitudeSumManager;
}
public GpsStatusValue getGpsStatus() {
return handlerServer.getGpsStatus();
public LiveData<GpsStatusValue> getGpsStatusObservable() {
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
@@ -717,6 +733,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
public class Binder extends android.os.Binder {
private Binder() {
@@ -727,4 +744,52 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
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;
/**
* 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
class GpsStatus {
@@ -56,6 +56,7 @@ class GpsStatus {
public void stop() {
stopped = true;
sendStatus(gpsStatus, GpsStatusValue.GPS_NONE);
}
}
@@ -75,10 +76,15 @@ class GpsStatus {
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.
*/
public void stop() {
client.onGpsStatusChanged(gpsStatus, GpsStatusValue.GPS_NONE);
client = null;
if (gpsStatusRunner != null) {
gpsStatusRunner.stop();
@@ -20,4 +20,8 @@ public enum GpsStatusValue {
this.icon = icon;
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);
}
public GpsStatusValue getGpsStatus() {
return locationHandler.getGpsStatus();
}
public interface HandlerServerInterface {
void newTrackPoint(TrackPoint trackPoint, int gpsAccuracy);
void newGpsStatus(GpsStatusValue gpsStatusValue);
@@ -38,6 +38,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
gpsStatus = new GpsStatus(context, this);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
registerLocationListener();
gpsStatus.start();
}
@Override
@@ -23,6 +23,8 @@ import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
@@ -135,7 +137,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
@Override
public void run(TrackRecordingService trackRecordingService) {
public void run(@NonNull TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
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.
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;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
@@ -35,7 +37,7 @@ public interface PeriodicTask {
*
* @param trackRecordingService the track recording service
*/
void run(TrackRecordingService trackRecordingService);
void run(@NonNull TrackRecordingService trackRecordingService);
/**
* Shuts down this task and clean up resources.
@@ -17,6 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.R;
@@ -52,14 +55,13 @@ public class PeriodicTaskExecutor {
private boolean metricUnits;
// The next distance for the distance periodic task
private double nextTaskDistance = Double.MAX_VALUE;
private Distance nextTaskDistance = Distance.of(Double.MAX_VALUE);
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
public PeriodicTaskExecutor(@NonNull TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService;
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;
}
@@ -97,7 +99,7 @@ public class PeriodicTaskExecutor {
timerTaskExecutor.scheduleTask(Duration.ofMinutes(taskFrequency));
} else {
// For distance periodic task
calculateNextTaskDistance();
updateNextTaskDistance();
}
}
@@ -128,59 +130,48 @@ public class PeriodicTaskExecutor {
return;
}
Distance distance = trackStatistics.getTotalDistance();
if (distance.greaterThan(Distance.of(nextTaskDistance))) {
if (trackStatistics.getTotalDistance().greaterThan(nextTaskDistance)) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
updateNextTaskDistance();
}
}
/**
* Sets task frequency.
*
* @param taskFrequency the task frequency
*/
public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency;
restore();
}
/**
* Sets metricUnits.
*
* @param metricUnits true to use metric units
*/
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
updateNextTaskDistance();
}
/**
* Calculates the next distance for the distance periodic task.
*/
private void calculateNextTaskDistance() {
private void updateNextTaskDistance() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Distance.of(Double.MAX_VALUE);
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
nextTaskDistance = calculateNextTaskDistance(trackStatistics);
}
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.
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Distance announcementInterval = Distance.one(metricUnits).multipliedBy(Math.abs(taskFrequency));
int index = (int) (distance.dividedBy(announcementInterval));
return announcementInterval.multipliedBy(index + 1);
}
/**
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import androidx.annotation.NonNull;
/**
* 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
*/
@NonNull
PeriodicTask create(Context context);
}
@@ -20,12 +20,10 @@ import java.util.Locale;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingService;
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.BluetoothLeCyclingPowerPreference;
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();
}
public static class PrefsFragment extends PreferenceFragmentCompat implements TrackRecordingServiceStatus.Listener {
public static class PrefsFragment extends PreferenceFragmentCompat {
private SharedPreferences sharedPreferences;
@@ -107,15 +105,20 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean isRecording;
private TrackRecordingService.RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final Runnable bindServiceCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(PrefsFragment.this);
if (service == null) {
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 exportPreference = findPreference(getString(R.string.settings_export));
boolean isRecording = recordingStatus.isRecording();
resetPreference.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) : "");
@@ -292,10 +296,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
});
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
if (trackId != null && !isRecording && isAdded()) {
isRecording = true;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
this.recordingStatus = status;
if (!status.isRecording() && isAdded()) {
updatePrefsDependOnRecording();
}
}
@@ -148,7 +148,7 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude());
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
@@ -49,7 +49,7 @@ public class AnnouncementUtils {
}
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);
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:padding="20dp">
<ImageView
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"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/app_name"
android:textSize="24sp" />
android:orientation="horizontal"
android:layout_margin="8dp">
<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
android:id="@+id/about_text_description"
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp" />
<TextView
android:id="@+id/about_text_description"
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="12dp" />
</LinearLayout>
<View style="@style/StatsHorizontalLine" />
@@ -74,12 +71,28 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="6dp"
android:linksClickable="true"
android:padding="6dp"
android:text="@string/about_history" />
<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
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -281,6 +281,17 @@
app:layout_constraintStart_toEndOf="@id/guideline"
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
android:id="@+id/stats_altitude_current_value"
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_distance_notification">Afstand: %1$s</string>
<string name="track_speed_notification">Hastighed: %1$s</string>
<string name="about_third_party_data">Data fra tredjeparter</string>
</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_speed_notification">Kiirus: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
</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_speed_notification">Abiadura: %1$s</string>
<string name="track_pace_notification">Erritmoa: %1$s</string>
<string name="about_third_party_data">Hirugarrenen datuak</string>
</resources>
+9 -7
View File
@@ -55,13 +55,14 @@ limitations under the License.
\n
\nJos kaikki ei auta, ilmoita virheestä. </string>
<string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string>
<string name="help_support_content"> Voit:
\n a) levittää sanaa OpenTracksista,
\n b) <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 d) auttaa ohjelmiston kehittämisessä,
\n e) ostaa sovelluksen <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>
<string name="help_support_content"> Sinä pystyt:
\n \u0020a) levittää sanaa OpenTracksista,
\n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ehdottaa ominaisuuksia </a>,
\n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ilmoita virheistä </a>,
\n \u0020d) auttaa ohjelmistokehityksessä,
\n \u0020e) osta <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play -kaupasta </a>,
\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_as_KMZ">Avaa KMZ-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_pace_notification">Vauhti: %1$s</string>
<string name="track_speed_notification">Nopeus: %1$s</string>
<string name="about_third_party_data">Kolmannen osapuolen tiedot</string>
</resources>
+8 -8
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_power">Potenza (W)</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_total_distance">Distanza totale: %1$.2f km (%2$.1f mi)</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_altitude">Altitudine</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_latitude">Latitudine</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>
<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="export_with_photos">Con le foto</string>
<string name="export_without_photos">Senza foto</string>
<string name="export_with_photos">con 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>
<plurals name="generic_completed_with_errors">
<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="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_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_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_day">Chiaro</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="value_pace_kilometer">%1$d:%2$02d min/cm</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="description_altitude_loss">Perdita di quota: %1$d m (%2$d ft)</string>
<string name="sensor_state_heart_rate_avg">Tasso cardiaco medio</string>
@@ -618,4 +618,4 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="track_distance_notification">Distanza: %1$s</string>
<string name="track_speed_notification">Velocità: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string>
</resources>
</resources>
+124 -10
View File
@@ -157,9 +157,9 @@ limitations under the License.
<string name="gps_disabled">GPS-модуль не включен. Нажмите здесь, чтобы открыть \"%1$s\". Нажмите \"Назад\", чтобы вернуться.</string>
<string name="gps_disabled_my_location">Не удалось определить местоположение. Добавьте другие источники передачи данных в настройках (%1$s) и убедитесь, что приему сигнала ничего не препятствует. Затем повторите попытку.</string>
<string name="gps_not_found">GPS-модуль не найден</string>
<string name="gps_starting">Включение GPS-модуля…</string>
<string name="gps_wait_for_better_signal">Поиск спутников GPS…</string>
<string name="gps_wait_for_signal">Ожидание сигнала GPS…</string>
<string name="gps_starting">Включение GPS-модуля</string>
<string name="gps_wait_for_better_signal">Поиск спутников GPS</string>
<string name="gps_wait_for_signal">Ожидание сигнала GPS</string>
<!-- Image -->
<string name="image_application">Приложение</string>
<string name="image_arrow">Стрелка</string>
@@ -172,7 +172,7 @@ limitations under the License.
<string name="image_stop">Стоп</string>
<string name="image_track">Трек</string>
<!-- 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_file">Нет файлов для импорта. Подсоедините устройство к компьютеру и укажите путь к файлам, которые нужно импортировать в %1$s.</string>
<string name="import_no_file_title">Импорт файлов</string>
@@ -278,7 +278,9 @@ limitations under the License.
<string name="settings_stats_units_title">Система измерений</string>
<!-- Share Track -->
<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>
<!-- Stats -->
<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_kilometer">%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_integer_feet">%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_poor_gps">%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_minute">%1$d мин.</string>
<string name="value_integer_minute">%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_off">Выкл.</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_two">2 мили в час</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_many">Общая длина: %1$.2f километров</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_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="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>
+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_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 -->
<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
+20
View File
@@ -219,6 +219,26 @@
<item>@string/settings_recording_track_name_number_option</item>
</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>
<!-- See TrackFileFormat -->
<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_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>
<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_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_third_party_data">Third-party data</string>
<string name="about_libraries">Libraries</string>
<!-- Help -->
<string name="help_feature_title">OpenTracks has the following features:</string>