Compare commits

...

65 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
Dennis Guse 1672a5c722 Release: v3.17.3 2021-04-27 21:03:44 +02:00
Román Martínez bf552a9219 Bugfix: crash deleting when activity restarts. Fixes #705. 2021-04-27 19:54:02 +02:00
Dennis Guse 348f697b56 TrackRecordingActivity needs to update TrackDataHub onResume().
Part of #719.
2021-04-27 19:02:53 +02:00
Dennis Guse 887d4700d4 TrackDataHub computes TrackStatistics and passes it to listeners.
Part of #719.
2021-04-27 19:02:53 +02:00
Dennis Guse 391dfd30e2 TrackDataHub does not need to register for status changes of TrackRecordingService.
This is handled by TrackRecordingActivity alone.
Part of #719.
2021-04-27 19:02:53 +02:00
Dennis Guse b416857001 TrackRecordingService log warnings if unexpected methods are invoked. 2021-04-27 19:02:53 +02:00
Dennis Guse df3720ef0d Bugfix: TrackDataManager.unregisterTrackDataListener was not removing listener. 2021-04-27 19:02:53 +02:00
Román Martínez b784a47af6 Bugfix: track stats always show pace. Fixes #726. 2021-04-25 15:21:08 +02:00
Dennis Guse 0f3156e71b i18n: French error message has no zero. 2021-04-22 18:36:45 +02:00
Dennis Guse 0035c85922 AboutActivity: back via top left arrow is working now.
Fixes #727.
2021-04-22 18:23:18 +02:00
Román Martínez 976a50ca15 Bugfixed spanish translation. Fixes #725. 2021-04-22 09:52:46 +02:00
mondstern c0ad53eef1 Translated using Weblate (Portuguese)
Currently translated at 78.6% (416 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt/
2021-04-21 19:57:41 +02:00
Quang Trung 1192b02c0a Translated using Weblate (Vietnamese)
Currently translated at 78.6% (416 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-04-21 19:57:41 +02:00
mondstern 7a3abe9962 Translated using Weblate (Hungarian)
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/hu/
2021-04-21 19:57:41 +02:00
mondstern 60b989c13a Translated using Weblate (French)
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/fr/
2021-04-21 19:57:41 +02:00
mondstern 340f16dc99 Translated using Weblate (French)
Currently translated at 91.1% (482 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-04-21 19:57:41 +02:00
Max Schallert 7cf29f7cea Translated using Weblate (German)
Currently translated at 95.4% (505 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-04-21 19:57:41 +02:00
dial25sd e39352cc9e Translated using Weblate (German)
Currently translated at 95.4% (505 of 529 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-04-21 19:57:41 +02:00
Dennis Guse e2964e805e Bugfix: NPE in voice announcements using pace.
Fixes #724.
2021-04-21 19:57:29 +02:00
Dennis Guse 8cab43d558 Release: v3.17.2 2021-04-19 19:38:31 +02:00
Dennis Guse 13f2a09652 Bugfix: AnnouncementPeriodicTask does not need to register as TrackRecordingService.Listener. 2021-04-19 19:29:51 +02:00
Dennis Guse 7d4072c0d8 Cleanup: ChartPoint uses null instead of NaN. 2021-04-18 17:50:16 +02:00
Dennis Guse 310b35713d ChartFragmentTest: added test for small changes. 2021-04-18 14:39:48 +02:00
Dennis Guse 7e6237de3a Release: v3.17.1 2021-04-17 22:40:05 +02:00
Román Martínez 772dad4837 announce called after getting recordingTrackId. Fixed a potencial race condition. 2021-04-17 22:26:23 +02:00
Román Martínez 99126fa357 Distance interval bugfixed: interval options are km/mi values. 2021-04-17 21:16:32 +02:00
Román Martínez b2c91d7592 registerListener for barometric sensor uses SENSOR_DELAY_FASTEST instead of nanoseconds. Fixes #714. 2021-04-17 19:34:05 +02:00
Rodrigo Oliveira 3c5a1be16b Fix #711: declare TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE on queries element
This enables the use of TextToSpeech engine on Android 11.
Ref.: https://developer.android.com/reference/android/speech/tts/TextToSpeech
2021-04-17 19:31:26 +02:00
92 changed files with 40276 additions and 1366 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 3981
versionName "v3.17.0"
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,7 @@
v3.17.1: OpenTracks
Bugfix:
- Interval length in meters instead of kilometers (introduced in v3.17.0)
- Elevation gain/loss not recorded (introduced in v3.17.0)
- Announcements not working on Android 11
@@ -0,0 +1,8 @@
v3.17.2: OpenTracks
WARNING: The v3.17.0+ releases contain a memory leak.
This issues is NOT FIXED by this release.
It is recommended to not upgrade if you still use a release before v3.17.0
Bugfix:
- Crash while using voice announcements
@@ -0,0 +1,11 @@
v3.17.3: OpenTracks
WARNING: The v3.17.0+ releases contain a memory leak.
This issues is NOT FIXED by this release.
It is recommended to not upgrade if you still use a release before v3.17.0
Bugfix:
- Crash if using pace for voice announcements (introduced in v3.17.0)
- Statistics showed pace initially (introduced in v3.17.0)
- Rare crash when deleting tracks
@@ -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
@@ -0,0 +1,94 @@
package de.dennisguse.opentracks.chart;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
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.fragments.TrackStubUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
public class ChartPointTest {
@Test
public void create_by_time() {
// given
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalTime(Duration.ofSeconds(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, false, false);
// then
assertEquals(1000000, (long) point.getTimeOrDistance());
}
@Test
public void create_by_distance() {
// given
TrackStatistics statistics = new TrackStatistics();
statistics.setTotalDistance(Distance.of(1000));
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, true, true);
// then
assertEquals(1, (long) point.getTimeOrDistance());
}
@Test
public void create_get_altitude_speed_and_pace() {
// given
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
// then
assertEquals(50, point.getAltitude(), 0.01);
assertEquals(36, point.getSpeed(), 0.01);
assertEquals(1.66, point.getPace(), 0.01);
}
@Test
public void create_sensorNotAvailable() {
// given
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true);
// then
assertNull(point.getHeartRate());
assertNull(point.getCadence());
assertNull(point.getPower());
}
@Test
public void create_sensorAvailable() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setHeartRate_bpm(100f);
trackPoint.setCyclingCadence_rpm(101f);
trackPoint.setPower(102f);
TrackStatistics statistics = new TrackStatistics();
// when
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), 50, false, true);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
assertEquals(101.0, point.getCadence(), 0.01);
assertEquals(102.0, point.getPower(), 0.01);
}
}
@@ -44,7 +44,7 @@ public class ChartValueSeriesTest {
ApplicationProvider.getApplicationContext(),
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{100, 1000},
new int[]{1, 100, 1000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
@@ -52,7 +52,7 @@ public class ChartValueSeriesTest {
15,
18) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getAltitude();
}
@@ -77,6 +77,17 @@ public class ChartValueSeriesTest {
assertFalse(series.isEnabled());
}
@Test
public void testVerySmallUpdates() {
series.update(new ChartPoint(1f));
series.update(new ChartPoint(2f));
series.update(new ChartPoint(3f));
series.updateDimension();
assertEquals(1, series.getInterval());
assertEquals(1, series.getMinMarkerValue());
assertEquals(6, series.getMaxMarkerValue());
}
@Test
public void testSmallUpdates() {
series.update(new ChartPoint(0));
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.fragments;
import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
@@ -28,23 +27,14 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
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.util.UnitConversions;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link ChartFragment}.
*
* @author Youtao Liu
*/
//TODO Add tests that check ChartFragment
@RunWith(AndroidJUnit4.class)
public class ChartFragmentTest {
@@ -68,264 +58,10 @@ public class ChartFragmentTest {
boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
}
/**
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testCreatePendingPoint_sensorIncorrect() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
// then
assertEquals(Float.NaN, point.getHeartRate(), 0.01);
assertEquals(Float.NaN, point.getCadence(), 0.01);
assertEquals(Float.NaN, point.getPower(), 0.01);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testCreatePendingPoint_sensorCorrect() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setHeartRate_bpm(100f);
trackPoint.setCyclingCadence_rpm(101f);
trackPoint.setPower(102f);
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
assertEquals(101.0, point.getCadence(), 0.01);
assertEquals(102.0, point.getPower(), 0.01);
}
/**
* Tests the logic to get the value of metric Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testCreatePendingPoint_distanceMetric() {
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second is a same location, just different time.
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use metric.
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint3);
// Computes the distance between Latitude 22 and 23.
float[] results = new float[4];
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(),
trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The fourth location is a new location, and use metric.
TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint();
trackPoint4.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint4);
// Computes the distance between Latitude 23 and 24.
Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(),
trackPoint4.getLatitude(), trackPoint4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testCreatePendingPoint_distanceImperial() {
// By distance.
chartFragment.setChartByDistance(true);
// Setups to use imperial.
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second location is a new location, and use imperial.
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint2);
/*
* Computes the distance between Latitude 22 and 23.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
float[] results = new float[4];
Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use imperial.
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint3);
/*
* Computes the distance between Latitude 23 and 24.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the values of time in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testCreatePendingPoint_time() {
// given
chartFragment.setChartByDistance(false);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME)); //Keep old TrackPoint behavior of having time=0 for this test
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
// then
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
Duration timeSpan = Duration.ofMillis(222);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setTime(Instant.ofEpochMilli(TrackStubUtils.INITIAL_TIME).plus(timeSpan));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(timeSpan, Duration.ofMillis((long) point.getTimeOrDistance()));
}
/**
* Tests the logic to get the value of altitude in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
*/
@Test
public void testCreatePendingPoint_altitude() {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
/*
* At first, clear old points of altitude, so give true to the second parameter.
* Then only one value INITIAL_ALTITUDE in buffer.
*/
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getAltitude(), 0.01);
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getAltitude(), 0.01);
}
/**
* Tests the logic to get the value of speed in {@link ChartFragment#createPendingPoint(TrackPoint)}.
* In this test, firstly remove all points in memory, and then fill in two points one by one.
* The speed values of these points are 129, 130.
*/
@Test
public void testCreatePendingPoint_speed() {
/*
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(Speed.of(128.5f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
/*
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
trackPoint2.setSpeed(Speed.of(130f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
}
/**
* Tests the logic to compute speed when use Imperial.
*/
@Test
public void testCreatePendingPoint_speedImperial() {
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(Speed.of(102f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false.
*/
@Test
public void testCreatePendingPoint_pace_nonZeroSpeed() {
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(Speed.of(102f));
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false and average speed is zero.
*/
@Test
public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(Speed.of(0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
assertEquals(0.0, point.getPace(), 0.01);
public void nothing() {
//TODO
}
}
@@ -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));
@@ -27,7 +27,7 @@ public class AnnouncementUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void getAnnouncement_metric() {
public void getAnnouncement_metric_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
@@ -43,7 +43,7 @@ public class AnnouncementUtilsTest {
}
@Test
public void getAnnouncement_withInterval_metric() {
public void getAnnouncement_withInterval_metric_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
@@ -61,4 +61,112 @@ public class AnnouncementUtilsTest {
// then
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour Lap speed of 51.2 kilometers per hour", announcement);
}
@Test
public void getAnnouncement_metric_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, null);
// then
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer", announcement);
}
@Test
public void getAnnouncement_withInterval_metric_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval);
// then
assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer Lap time of 1 minute 10 seconds per kilometer", announcement);
}
@Test
public void getAnnouncement_imperial_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, null);
// then
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour", announcement);
}
@Test
public void getAnnouncement_withInterval_imperial_speed() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval);
// then
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour Lap speed of 31.8 miles per hour", announcement);
}
@Test
public void getAnnouncement_imperial_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, null);
// then
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile", announcement);
}
@Test
public void getAnnouncement_withInterval_imperial_pace() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval);
// then
assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement);
}
}
@@ -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);
}
}
}
+6
View File
@@ -351,4 +351,10 @@ limitations under the License.
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest>
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import androidx.annotation.Nullable;
@@ -32,9 +31,4 @@ public class AboutActivity extends AbstractActivity {
viewBinding = AboutBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return false;
}
}
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks;
import android.content.Intent;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -29,13 +31,15 @@ import de.dennisguse.opentracks.util.IntentUtils;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}:
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
* <p>
* - share track <br>
* - delete tracks <br>
*
* @author Jimmy Shih
*/
//TODO It is actually not a List; we need a better name for this class
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
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();
}
}
@@ -64,26 +64,29 @@ public class TrackDeleteActivity extends AbstractActivity {
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
});
}
@Override
protected void onStart() {
super.onStart();
deleteThread.start();
}
@Override
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
protected void onStart() {
super.onStart();
if (deleteThread.getState() == Thread.State.TERMINATED) {
onAsyncTaskCompleted();
}
}
@Override
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected View getRootView() {
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
@@ -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.
@@ -53,7 +55,8 @@ import de.dennisguse.opentracks.util.IntentUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface, TrackRecordingServiceStatus.Listener {
//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 {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
@@ -66,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;
@@ -78,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));
}
}
};
@@ -89,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);
@@ -109,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();
}
@@ -131,6 +156,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
super.onStop();
trackRecordingServiceConnection.unbind(this);
trackDataHub.stop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
@@ -161,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);
@@ -216,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);
@@ -299,13 +326,12 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
}
}
public void startPostponedEnterTransitionWith(View viewIcon, View viewName) {
public void startPostponedEnterTransitionWith(View viewIcon) {
ViewCompat.setTransitionName(viewIcon, TrackRecordedActivity.VIEW_TRACK_ICON);
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,10 +89,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on.
trackDataHub.loadTrack(trackId);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
}
service.addListener(TrackRecordingActivity.this);
}
};
@@ -106,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);
}
};
@@ -205,11 +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);
trackController.onResume(true, recordingTrackPaused);
trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
}
/*
@@ -260,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);
}
@@ -335,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
@@ -400,21 +403,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
if (recordingTrackPaused != isPaused) {
trackController.update(true, isPaused);
}
recordingTrackPaused = isPaused;
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
trackController.update(recordingStatus);
trackDataHub.setRecordingStatus(recordingStatus);
setLockscreenPolicy();
setScreenOnPolicy();
}
@Override
public void onTrackRecordingId(Track.Id newTrackId) {
if (newTrackId != null && !newTrackId.equals(trackId)) {
trackId = newTrackId;
trackController.update(true, recordingTrackPaused);
}
}
}
@@ -3,9 +3,10 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
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.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions;
public class ChartPoint {
@@ -13,44 +14,38 @@ public class ChartPoint {
private double timeOrDistance;
//Y-axis
private double altitude;
private double speed;
private double pace;
private double heartRate = Double.NaN;
private double cadence = Double.NaN;
private double power = Double.NaN;
private final double altitude;
private Double speed;
private Double pace;
private Double heartRate;
private Double cadence;
private Double power;
@Deprecated
@VisibleForTesting
ChartPoint(double altitude) {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPoint trackPoint, boolean chartByDistance, boolean metricUnits) {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
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 = trackStatisticsUpdater.getSmoothedAltitude();
if (!metricUnits) {
altitude *= UnitConversions.M_TO_FT;
}
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
speed = trackStatisticsUpdater.getSmoothedSpeed().to(metricUnits);
pace = trackStatisticsUpdater.getSmoothedSpeed().toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (trackPoint != null) {
if (trackPoint.hasHeartRate()) {
heartRate = trackPoint.getHeartRate_bpm();
}
if (trackPoint.hasCyclingCadence()) {
cadence = trackPoint.getCyclingCadence_rpm();
}
if (trackPoint.hasPower()) {
power = trackPoint.getPower();
}
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate_bpm();
}
if (trackPoint.hasCyclingCadence()) {
cadence = (double) trackPoint.getCyclingCadence_rpm();
}
if (trackPoint.hasPower()) {
power = (double) trackPoint.getPower();
}
}
@@ -58,39 +53,32 @@ public class ChartPoint {
return timeOrDistance;
}
public double getAltitude() {
public Double getAltitude() {
return altitude;
}
public double getSpeed() {
public Double getSpeed() {
return speed;
}
public double getPace() {
public Double getPace() {
return pace;
}
public boolean hasHeartRate() {
return Double.isNaN(heartRate);
}
public double getHeartRate() {
public Double getHeartRate() {
return heartRate;
}
public boolean hasCadence() {
return Double.isNaN(cadence);
}
public double getCadence() {
public Double getCadence() {
return cadence;
}
public boolean hasPower() {
return Double.isNaN(power);
}
public double getPower() {
public Double getPower() {
return power;
}
@Override
public String toString() {
return "ChartPoint{" + "timeOrDistance=" + timeOrDistance + '}';
}
}
@@ -132,10 +132,10 @@ abstract class ChartValueSeries {
}
}
abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
abstract Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
boolean isChartPointValid(@NonNull ChartPoint chartPoint) {
return !Double.isNaN(extractDataFromChartPoint(chartPoint));
return extractDataFromChartPoint(chartPoint) != null;
}
protected abstract boolean drawIfChartPointHasNoData();
@@ -206,7 +206,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getAltitude();
}
@@ -227,7 +227,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
}
@@ -249,7 +249,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
}
@@ -271,7 +271,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
}
@@ -292,7 +292,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
}
@@ -312,7 +312,7 @@ public class ChartView extends View {
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
}
@@ -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,12 @@ 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;
import de.dennisguse.opentracks.content.data.Track;
@@ -39,8 +42,8 @@ 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.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.EGM2008Utils;
/**
* Track data hub.
@@ -50,7 +53,8 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
//TODO register contentobserver only for exact URL (incl. trackId) to not filter here.
public class TrackDataHub {
/**
* Target number of track points displayed by the diagrams (recommended).
@@ -71,34 +75,29 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
private final ContentProviderUtils contentProviderUtils;
private final int targetNumPoints;
private EGM2008Utils.EGM2008Correction egm2008Correction;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
// Preference values
private Track.Id selectedTrackId;
private Track.Id recordingTrackId;
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;
private ContentObserver markersTableObserver;
private ContentObserver trackPointsTableObserver;
private final Runnable bindCallback = () -> {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service != null) {
service.addListener(TrackDataHub.this);
}
};
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACKPOINTS);
}
@@ -147,15 +146,6 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
trackRecordingServiceConnection.bind(context);
handler.post(() -> {
if (started) {
loadDataForAll();
}
});
}
public void stop() {
@@ -165,6 +155,7 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
}
started = false;
trackStatisticsUpdater = null;
//Unregister listeners
ContentResolver contentResolver = context.getContentResolver();
@@ -177,8 +168,6 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
handlerThread = null;
}
handler = null;
trackRecordingServiceConnection.unbind(context);
}
public void loadTrack(final @NonNull Track.Id trackId) {
@@ -219,14 +208,14 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
* 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();
}
/**
@@ -374,6 +363,8 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
break;
}
correctAltitude(trackPoint);
if (localFirstSeenTrackPointId == null) {
localFirstSeenTrackPointId = trackPointId;
}
@@ -383,15 +374,16 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
// Also include the last point if the selected track is not recording.
if ((localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(trackPoint);
trackDataListener.onSampledInTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics(), trackStatisticsUpdater.getSmoothedSpeed(), trackStatisticsUpdater.getSmoothedAltitude());
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(trackPoint);
trackDataListener.onSampledOutTrackPoint(trackPoint, trackStatisticsUpdater.getTrackStatistics());
}
}
@@ -411,11 +403,29 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
if (trackPoint != null) {
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone(trackPoint);
listener.onNewTrackPointsDone(trackPoint, trackStatisticsUpdater.getTrackStatistics());
}
}
}
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.
*/
@@ -423,15 +433,14 @@ public class TrackDataHub implements TrackRecordingServiceStatus.Listener {
numLoadedPoints = 0;
firstSeenTrackPointId = null;
lastSeenTrackPointId = null;
trackStatisticsUpdater = new TrackStatisticsUpdater();
}
@Override
public void onTrackRecordingPaused(boolean isPaused) {
recordingTrackPaused = isPaused;
public void setRecordingStatus(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
}
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
public void setRecordingDistanceInterval(Distance recordingDistanceInterval) {
this.recordingDistanceInterval = recordingDistanceInterval;
}
}
@@ -19,8 +19,10 @@ package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Listener for track data changes.
@@ -47,20 +49,23 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(@NonNull TrackPoint trackPoint);
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
}
/**
* Called when a sampled out track point is read.
*
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint);
default void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics) {
}
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint, TrackStatistics, Speed, double)} and {@link #onSampledOutTrackPoint(TrackPoint, TrackStatistics)}.
*/
void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint);
default void onNewTrackPointsDone(@NonNull TrackPoint lastTrackPoint, @NonNull TrackStatistics trackStatistics) {
}
/**
* Called to clear previously sent markers.
@@ -41,10 +41,10 @@ class TrackDataManager {
}
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
dataListenerTracks.add(trackDataListener);
dataListenerMarkers.add(trackDataListener);
dataListenerTrackPoints_SampledIn.add(trackDataListener);
dataListenerTrackPoints_SampledOut.add(trackDataListener);
dataListenerTracks.remove(trackDataListener);
dataListenerMarkers.remove(trackDataListener);
dataListenerTrackPoints_SampledIn.remove(trackDataListener);
dataListenerTrackPoints_SampledOut.remove(trackDataListener);
}
boolean hasListeners() {
@@ -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);
}
}
}
@@ -20,9 +20,13 @@ public class Distance {
return of(distance_mile * UnitConversions.MI_TO_M);
}
public static Distance ofKilometer(double distance_km) {
return of(distance_km * UnitConversions.KM_TO_M);
}
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.of(1);
return Distance.ofKilometer(1);
} else {
return Distance.ofMile(1);
}
@@ -94,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());
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
@@ -72,7 +73,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
private final List<ChartPoint> pendingPoints = new ArrayList<>();
private String category = "";
@@ -217,7 +217,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void clearTrackPoints() {
if (isResumed()) {
trackStatisticsUpdater = new TrackStatisticsUpdater();
pendingPoints.clear();
chartView.reset();
runOnUiThread(() -> {
@@ -228,24 +227,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
@Override
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
if (isResumed()) {
pendingPoints.add(createPendingPoint(trackPoint));
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, chartView.getMetricUnits());
pendingPoints.add(point);
}
}
@Override
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
if (isResumed()) {
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
}
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
if (isResumed()) {
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
@@ -306,6 +296,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
* Returns true if the selected track is recording.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
@Deprecated
//TODO Should not be dynamic but instead set while instantiating, i.e., newFragment().
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
@@ -322,34 +314,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
@VisibleForTesting
ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
return new ChartPoint(trackStatisticsUpdater, trackPoint, chartByDistance, chartView.getMetricUnits());
}
@VisibleForTesting
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;
}
}
@@ -25,9 +25,11 @@ import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
@@ -229,21 +231,21 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused, Speed unused2, double unused3) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused) {
if (isResumed()) {
viewModel.add(trackPoint);
}
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
public void onNewTrackPointsDone(@NonNull TrackPoint unused, @NonNull TrackStatistics alsoUnused) {
if (isResumed()) {
runOnUiThread(viewModel::onNewTrackPoints);
}
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
@@ -54,6 +55,8 @@ import de.dennisguse.opentracks.viewmodels.SensorDataModel;
*/
public class StatisticsRecordedFragment extends Fragment {
private static final String TAG = StatisticsRecordedFragment.class.getSimpleName();
private static final String TRACK_ID_KEY = "trackId";
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
@@ -154,19 +157,25 @@ public class StatisticsRecordedFragment extends Fragment {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
if ((this.track == null && track != null) || (this.track != null && track != null && !this.track.getCategory().equals(track.getCategory()))) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
if (track == null) {
Log.e(TAG, "track cannot be null");
getActivity().finish();
return;
}
sensorStatistics = contentProviderUtils.getSensorStats(trackId);
boolean prefsChanged = this.track == null || (!this.track.getCategory().equals(track.getCategory()));
this.track = track;
if (prefsChanged) {
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, getString(R.string.stats_rate_key));
}
loadTrackDescription(track);
updateUI();
updateSensorUI();
((TrackRecordedActivity) getActivity()).startPostponedEnterTransitionWith(viewBinding.statsActivityTypeIcon, viewBinding.statsNameValue);
((TrackRecordedActivity) getActivity()).startPostponedEnterTransitionWith(viewBinding.statsActivityTypeIcon);
}
});
}
@@ -34,6 +34,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -211,17 +212,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
@Override
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint) {
// We don't care.
}
@Override
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint) {
public void onNewTrackPointsDone(@NonNull TrackPoint newLastTrackPoint, @NonNull TrackStatistics trackStatistics) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
@@ -388,7 +379,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
// Set time
if (track != null && track.getTrackStatistics() != null) {
if (track != null) {
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
updateTotalTime();
}
@@ -453,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,15 +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 recordingStatus.getTrackId();
}
public TrackStatistics getTrackStatistics() {
@@ -219,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;
@@ -240,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));
}
@@ -252,7 +268,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
*/
public Track.Id startNewTrack() {
if (isRecording()) {
Log.d(TAG, "Ignore startNewTrack. Already recording.");
Log.w(TAG, "Ignore startNewTrack. Already recording.");
return null;
}
@@ -262,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);
@@ -304,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();
}
@@ -312,15 +328,15 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void resumeCurrentTrack() {
if (!isRecording() || !isPaused()) {
Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
Log.w(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
return;
}
// 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());
}
@@ -361,18 +377,18 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public Track.Id endCurrentTrack() {
public void endCurrentTrack() {
if (!isRecording()) {
Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
return null;
Log.w(TAG, "Ignore endCurrentTrack. Not recording.");
return;
}
// 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
@@ -390,22 +406,20 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
ExportUtils.postWorkoutExport(this, track, new ExportServiceResultReceiver(new Handler(), this));
endRecording(true);
return trackId;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void pauseCurrentTrack() {
if (!isRecording() || isPaused()) {
Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
Log.w(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
return;
}
// 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);
@@ -488,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;
@@ -562,11 +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 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);
}
}
/**
@@ -658,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)
@@ -694,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
@@ -709,6 +733,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
public class Binder extends android.os.Binder {
private Binder() {
@@ -719,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,79 +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<>();
public void addListener(@NonNull Listener listener) {
if (this.listeners.contains(listener)) {
return;
}
this.listeners.add(listener);
listener.onTrackRecordingId(recordingTrackId);
listener.onTrackRecordingPaused(recordingTrackPaused);
}
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
@@ -40,7 +40,7 @@ public class AltitudeSumManager implements SensorEventListener {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
} else {
isConnected = sensorManager.registerListener(this, pressureSensor, (int) SAMPLING_RATE.toNanos());
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
}
lastAcceptedPressureValue_hPa = Float.NaN;
@@ -23,7 +23,10 @@ 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;
import de.dennisguse.opentracks.content.data.Distance;
@@ -32,7 +35,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -58,8 +60,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
private final ContentProviderUtils contentProviderUtils;
private Track.Id recordingTrackId;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -137,18 +137,13 @@ 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;
}
trackRecordingService.addListener(new TrackRecordingServiceStatus.Listener() {
@Override
public void onTrackRecordingId(Track.Id trackId) {
recordingTrackId = trackId;
}
});
announce(trackRecordingService.getTrackStatistics());
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
}
/**
@@ -156,7 +151,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
*
* @param trackStatistics the track statistics
*/
private void announce(TrackStatistics trackStatistics) {
private void announce(Track.Id trackId, TrackStatistics trackStatistics) {
if (trackStatistics == null) {
Log.e(TAG, "TrackStatistics is null.");
return;
@@ -175,13 +170,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
}
if (audioManager.getMode() == AudioManager.MODE_IN_CALL || audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) {
if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION)
.contains(audioManager.getMode())) {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
Track track = contentProviderUtils.getTrack(recordingTrackId);
String category = track != null ? track.getCategory() : "";
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.i(TAG, "It doesn't exists a track with trackid = " + track);
return;
}
String category = track.getCategory();
//TODO Querying all TrackPoints all the time is inefficient; use TrackDataHub or something else.
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
@@ -233,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();
}
}
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.stats;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
@@ -193,6 +194,7 @@ public class TrackStatistics {
this.movingTime = movingTime;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void addMovingTime(Duration time) {
movingTime = movingTime.plus(time);
}
@@ -265,6 +267,7 @@ public class TrackStatistics {
this.totalAltitudeGain_m = totalAltitudeGain_m;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void addTotalAltitudeGain(float gain_m) {
if (totalAltitudeGain_m == null) {
totalAltitudeGain_m = 0f;
@@ -285,6 +288,7 @@ public class TrackStatistics {
this.totalAltitudeLoss_m = totalAltitudeLoss_m;
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void addTotalAltitudeLoss(float loss_m) {
if (totalAltitudeLoss_m == null) {
totalAltitudeLoss_m = 0f;
@@ -106,7 +106,7 @@ public class TrackStatisticsUpdater {
*/
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString());
Log.v(TAG, this.toString());
}
private void internalAddTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
@@ -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) {
@@ -43,13 +43,13 @@ public class AnnouncementUtils {
int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
rate = context.getString(paceId, getAnnounceTime(context, time));
Duration currentTime = currentDistancePerTime.toPace(isMetricUnits);
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(isMetricUnits) : Duration.ofMillis(0);
currentRate = context.getString(paceId, getAnnounceTime(context, currentTime));
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
}
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,7 +35,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
}
intervalsLiveData = new MutableLiveData<>();
distanceInterval = interval.getValue();
distanceInterval = interval.getDistance(metricUnits);
loadIntervalStatistics();
}
return intervalsLiveData;
@@ -73,7 +73,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
interval = IntervalOption.OPTION_1;
}
distanceInterval = interval.getValue();
distanceInterval = interval.getDistance(metricUnits);
loadIntervalStatistics();
}
}
@@ -91,19 +91,21 @@ public class IntervalStatisticsModel extends AndroidViewModel {
OPTION_20(20),
OPTION_50(50);
private final Distance value;
private final int multiplier;
IntervalOption(int value) {
this.value = Distance.of(value);
IntervalOption(int multiplier) {
this.multiplier = multiplier;
}
public Distance getValue() {
return value;
public Distance getDistance(boolean metricUnits) {
return Distance
.one(metricUnits)
.multipliedBy(multiplier);
}
@Override
public String toString() {
return "" + (int) value.toM(); //TODO Somehow IntervalsFragment relies on a parsable Integer.
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.
}
}
}
+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>
+5 -3
View File
@@ -217,8 +217,8 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="gps_location_access">\"Standortzugriff\"</string>
<string name="gps_disabled">GPS ist nicht aktiviert. Tippen Sie hier, um zu %1$s wechseln. Drücken Sie \"Zurück\", um zurückzugehen.</string>
<string name="gps_disabled_my_location">Der aktuelle Standort kann nicht ermittelt werden. Aktivieren Sie mehr Standortquellen unter \"%1$s\" und vergewissern Sie sich, dass der Himmel sichtbar ist. Bitte versuchen Sie es später erneut.</string>
<string name="gps_not_found">Das Gerät sendet kein GPS-Signal.</string>
<string name="gps_starting">GPS wird gestartet.</string>
<string name="gps_not_found">Das Gerät empfängt kein GPS-Signal</string>
<string name="gps_starting">GPS wird gestartet</string>
<string name="gps_wait_for_better_signal">Warten auf besseres GPS-Signal</string>
<string name="gps_wait_for_signal">Warten auf GPS-Signal</string>
<!-- Image -->
@@ -584,9 +584,11 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="export_progress_summary_overwrite_msg">Streckenaufzeichnung überschrieben</string>
<string name="export_progress_summary_skip_msg">Strecke übersprungen</string>
<string name="export_progress_summary_errors_msg">Fehler</string>
<string name="generic_click_twice_cancel">Klicke nochmal zurück um abzubrechen.</string>
<string name="generic_click_twice_cancel">Klicke nochmal auf zurück um abzubrechen</string>
<string name="import_error_list_dialog_title">Dateien wurden nicht importiert</string>
<string name="voice_speed_lap">Rundengeschwindigkeit %1$s</string>
<string name="voice_pace_lap">Rundenzeit %1$s</string>
<string name="toast_message_instant_track_was_exported">Letzte Aufzeichnung exportiert</string>
<string name="export_track_already_exists_msg">Der Track %1$s existiert bereits im Zielverzeichnis.</string>
<string name="gps_fixed_and_ready">GPS-Position bestimmt und bereit</string>
</resources>
+1 -1
View File
@@ -600,7 +600,7 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="sensor_state_cadence_max">Cadencia máxima</string>
<string name="sensor_state_cadence_avg">Cadencia media</string>
<string name="value_float_kilometer_hour">%1$.2f kp/h</string>
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
<string name="export_progress_summary_overwrite_msg">Pistas sobrescritas</string>
<string name="export_progress_summary_skip_msg">Pistas omitidas</string>
<string name="export_progress_summary_errors_msg">Errores</string>
+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>
+78 -3
View File
@@ -13,7 +13,8 @@ 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.
--><resources>
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="about_preference_title">À propos d\'OpenTracks</string>
<string name="about_description"><i>OpenTracks</i> est une application de suivi sportif qui respecte totalement votre vie privée.</string>
<string name="about_url">Page web du projet : <a href="%1$s">%1$s</a></string>
@@ -291,7 +292,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="sensor_state_cadence_value">%1$d tr/min</string>
<string name="sensor_state_disconnected">Déconnecté</string>
<string name="sensor_state_heart_rate">Fréquence cardiaque</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
<string name="sensor_state_heart_rate_value">%1$d ppm</string>
<string name="sensor_state_power">Puissance</string>
<string name="sensor_state_power_value">%1$d W</string>
<string name="sensor_not_known">Capteur %1$s est inconnu. Veuillez vérifier les paramètres.</string>
@@ -358,7 +359,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="stats_average_speed">Vitesse moyenne</string>
<string name="stats_altitude">Altitude</string>
<string name="stats_fastest_pace">Allure la plus rapide</string>
<string name="stats_gain">Gain</string>
<string name="stats_gain">Gagner</string>
<string name="stats_grade">Inclinaison</string>
<string name="stats_latitude">Latitude</string>
<string name="stats_longitude">Longitude</string>
@@ -537,4 +538,78 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<!-- Share image -->
<string name="share_image_subject">J aimerais partager une image avec vous</string>
<string name="share_image_body">Je pense que cette image pourrait vous intéresser.</string>
<string name="track_pace_notification">Vitesse : %1$s</string>
<string name="track_speed_notification">Vitesse : %1$s</string>
<string name="track_detail_intervals_tab">Intervalles</string>
<string name="interval_list_empty_message">Il n\'y a pas encore d\'intervalle</string>
<string name="aggregated_stats_empty_message">Enregistrez votre première piste pour voir les statistiques agrégées</string>
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
<string name="value_float_mile_hour">%1$.2f mph</string>
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="voice_pace_lap">Temps au tour de %1$s</string>
<string name="voice_speed_lap">Vitesse du tour de %1$s</string>
<string name="toast_message_instant_track_was_exported">Dernière piste exportée</string>
<string name="settings_default_export_uri_title">Répertoire d\'exportation des pistes</string>
<string name="instant_export_enabled_title">Exportation instantanée après l\'entraînement</string>
<string name="stats_intervals">Intervalles</string>
<string name="stats_loss">Perte</string>
<string name="settings_prevent_reimport_tracks_title">Empêcher la réimportation de pistes</string>
<string name="settings_night_mode_option_night">Nuit</string>
<string name="settings_night_mode_option_day">Jour</string>
<string name="settings_night_mode_option_system">Système</string>
<string name="stats_split_interval">Intervalle partagé</string>
<string name="instant_export_enabled_summary">Exportation de la piste vers le stockage après la fin de l\'enregistrement</string>
<string name="hold_to_pause">Tenir pour faire une pause</string>
<string name="hold_to_stop">Maintenir pour arrêter</string>
<string name="track_distance_notification">Distanciation : %1$s</string>
<string name="open_track_as_KMZ">Ouvert comme KMZ</string>
<string name="open_track_as_GPX">Ouvert en tant que GPX</string>
<string name="open_track_as_trackfileformat">Ouvrir comme %1$s</string>
<string name="open_track_using_dashboard">Ouvrir en utilisant OpenTracks-Dashboard</string>
<string name="activity_type_escooter">éscooter</string>
<string name="settings_sensor_wheel_circumference">Circonférence de roue (millimètre)</string>
<string name="export_error_post_workout">L\'exportation du post-travail a échoué, veuillez vérifier le répertoire d\'exportation.</string>
<string name="export_all_do_it_for_all">le faire pour tous les conflits</string>
<string name="export_track_errors">Pistes non exportées</string>
<string name="import_unable_to_import_file">Impossible d\'importer le fichier : %1$s</string>
<string name="import_no_kml_file_found">Pas de doc.fichier kml trouvé dans KMZ</string>
<string name="import_error_info">%1$s : %2$s</string>
<string name="import_error_list_dialog_title">Les fichiers ne sont pas importées</string>
<string name="settings_night_mode_title">Thème de l\'Interface utilisateur</string>
<string name="generic_completed">Compléter</string>
<string name="sensor_state_cadence_max">Cadence maximale</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Plein écran</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Pendant l\'enregistrement, présent en plein écran.</string>
<string name="sensor_state_power_avg">Puissance moyenne</string>
<string name="sensor_state_heart_rate_max">Fréquence cardiaque max</string>
<string name="sensor_state_heart_rate_avg">Rythme cardiaque moyen</string>
<string name="sensor_state_cadence_avg">Cadence moyenne</string>
<string name="generic_click_twice_cancel">Cliquez sur retour pour annuler</string>
<string name="export_progress_summary_new_msg">Nouvelles pistes exportées</string>
<string name="export_progress_summary_skip_msg">Pistes sautées</string>
<string name="export_progress_summary_errors_msg">Erreurs</string>
<string name="generic_overwrite">Écraser</string>
<string name="generic_show_errors">Afficher les erreurs</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_skip">Sauter</string>
<string name="generic_open_track">Voie ouverte</string>
<string name="gps_disabled_msg">GPS désactivé</string>
<string name="gps_fixed_and_ready">GPS fixe et prêt</string>
<string name="import_thread_interrupted">Fil interrompu</string>
<string name="import_unsupported_format">Format de fichier non pris en charge</string>
<string name="import_parser_error">Erreur de l\'analyseur : %1$s</string>
<string name="import_file_imported">Fichier %1$s importé</string>
<string name="import_prevent_reimport">Vous avez défini empêcher la réimportation de la piste</string>
<string name="import_progress_summary_ok_msg">Les fichiers importés</string>
<string name="import_progress_summary_exists_msg">Il existe déjà</string>
<string name="import_progress_summary_errors_msg">Erreur</string>
<plurals name="generic_completed_with_errors">
<item quantity="one" tools:ignore="ImpliedQuantity">Terminé avec une erreur</item>
<item quantity="other">Terminé avec des erreurs %1$d</item>
</plurals>
<string name="settings_default_trackfileformat">Format de fichier d\'exportation / partage</string>
<string name="description_altitude_loss">Perte d\'altitude : %1$d m (%2$d ft)</string>
<string name="export_track_already_exists_msg">La piste %1$s existe déjà dans le répertoire de destination.</string>
<string name="export_progress_summary_overwrite_msg">Pistes écrasées</string>
</resources>
+152 -9
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,8 +13,7 @@ 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.
-->
<resources>
--><resources>
<!-- Activity Type -->
<string name="activity_type_airplane">repülőgép</string>
<string name="activity_type_atv">terepjáró</string>
@@ -63,7 +63,7 @@ limitations under the License.
<string name="activity_type_skate_boarding">gördeszkázás</string>
<string name="activity_type_skating">korcsolyázás</string>
<string name="activity_type_skiing">síelés</string>
<string name="activity_type_sky_jumping">sky jumping</string>
<string name="activity_type_sky_jumping">égi ugrás</string>
<string name="activity_type_sled">szánkó</string>
<string name="activity_type_snow_boarding">snowboard</string>
<string name="activity_type_snow_shoeing">hótalpazás</string>
@@ -75,7 +75,7 @@ limitations under the License.
<string name="activity_type_trail_hiking">túrázás</string>
<string name="activity_type_trail_running">terepfutás</string>
<string name="activity_type_train">vonat</string>
<string name="activity_type_ultimate_frisbee">ultimate frisbee</string>
<string name="activity_type_ultimate_frisbee">ultimate frizbi</string>
<string name="activity_type_unknown">ismeretlen</string>
<string name="activity_type_wakeboarding">wakeboard</string>
<string name="activity_type_walking">gyaloglás</string>
@@ -147,7 +147,7 @@ limitations under the License.
<string name="generic_ok">OK</string>
<string name="generic_paused">Szüneteltetett</string>
<string name="generic_progress_title">Kis türelmet…</string>
<string name="generic_recording">Rögzítés</string>
<string name="generic_recording">Rögzítés…</string>
<string name="generic_save">Mentés</string>
<string name="generic_success_title">Sikerült</string>
<string name="generic_tracks">Számok</string>
@@ -277,10 +277,11 @@ limitations under the License.
<string name="settings_stats_units_imperial">Angolszász (mérföld, láb)</string>
<string name="settings_stats_units_metric">Metrikus (km, m)</string>
<string name="settings_stats_units_title">Mértékegység választása</string>
<!-- Share Track -->
<string name="share_track_share_file">Útvonal-fájl megosztása</string>
<string name="share_track_share_file_body">Szerintem érdekelni fog ez a pálya.\n\nA pályát a Google Earth vagy más térképszoftver segítségével nézheted meg.\n\n%1$s</string>
<string name="share_track_share_file_body">Szerintem érdekelni fog ez a pálya.
\n
\n%1$s</string>
<string name="share_track_subject">Szeretnék megosztani veled egy pályát</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Mozgás átlagos irama</string>
@@ -462,4 +463,146 @@ limitations under the License.
<string name="waypoint_type_waterfall">vízesés</string>
<string name="waypoint_type_water_fountain">szökőkút</string>
<string name="waypoint_type_waypoint">útpont</string>
</resources>
<string name="generic_overwrite">Felülírás</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_skip">Ugrás</string>
<string name="about_url">A projekt weboldala: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Verzió neve: %1$s</string>
<string name="about_version_code">Verziókód: %1$d</string>
<string name="about_preference_title">Az OpenTracks</string>
<string name="help_data_google_play_services_title">Az OpenTracks igényel/használja a Google Play szolgáltatást\?</string>
<string name="help_track_export_title">Hogyan tudom exportálni/megosztani a felvett zeneszámokat\?</string>
<string name="help_storage_pictures_content">Ezeket az alkalmazás belső tárhelyén tárolja (azaz csak az OpenTracks-en belül érhető el).</string>
<string name="about_libraries">Könyvtárak</string>
<string name="help_feature_title">Az OpenTracks a következő funkciókkal rendelkezik:</string>
<string name="help_storage_pictures_title">Hogyan tárolja az OpenTracks a jelölőként készített képeket\?</string>
<string name="help_track_map_title">Hogyan jelenítsünk meg egy pályát a térképen\?</string>
<string name="help_gps_troubleshooting_title">Az OpenTracks nem rögzíti a helyszíneket\?</string>
<string name="help_support_title">Hogyan tudom támogatni az OpenTracks-t\?</string>
<string name="open_track_using_dashboard">Megnyitás az OpenTracks-Dashboard segítségével</string>
<string name="open_track_as_KMZ">Megnyitás KMZ-ként</string>
<string name="open_track_as_GPX">Megnyitás GPX-ként</string>
<string name="open_track_as_trackfileformat">Megnyitás %1$s néven</string>
<string name="activity_type_escooter">eszkoter</string>
<string name="help_recording_content">Az OpenTracks a készülék GPS-érzékelőjét használja a hely folyamatos rögzítésére. Ha a GPS-eszköz pontatlan adatokat (pl. hely, sebesség, magasság) jelent, az OpenTracks nem tud mit tenni.</string>
<string name="help_data_storage_title">Hogyan tárolja az OpenTracks az érzékelőadatokat\?</string>
<string name="app_not_installed_show_on_map">Kérjük, telepítsen egy olyan alkalmazást, amely támogatja a térképadatok megjelenítését.</string>
<string name="import_error_list_dialog_title">Nem importált fájlok</string>
<string name="import_progress_summary_errors_msg">Hibák</string>
<string name="sensor_state_cadence_max">Max kadencia</string>
<string name="sensor_state_heart_rate_max">Átlagos pulzusszám</string>
<string name="stats_intervals">Intervallumok</string>
<string name="settings_sensor_wheel_circumference">Kerékkörfogat (mm)</string>
<string name="stats_split_interval">Osztott intervallum</string>
<string name="track_speed_notification">Sebesség: %1$s</string>
<string name="track_detail_intervals_tab">Intervallum</string>
<string name="track_detail_record_success">Most egy új szám felvétele</string>
<string name="track_detail_resume_success">Most folytatódik a pálya</string>
<string name="interval_list_empty_message">Még nincs intervallum</string>
<string name="value_float_mile_hour">%1$.2f mph</string>
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="settings_default_export_uri_title">Pálya export könyvtár</string>
<string name="instant_export_enabled_summary">Sáv exportálása a tárolóba a felvétel befejezése után</string>
<string name="toast_message_instant_track_was_exported">Utolsó exportált zeneszám</string>
<string name="hold_to_pause">Tartsa a szünethez</string>
<string name="hold_to_stop">Tartsa lenyomva a leállításhoz</string>
<string name="track_distance_notification">Távolság: %1$s</string>
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
<string name="instant_export_enabled_title">Azonnali edzés utáni export</string>
<string name="track_pace_notification">Tempó: %1$s</string>
<string name="export_with_photos">fotókkal</string>
<string name="export_error_post_workout">Az edzés utáni exportálás sikertelen, kérjük, ellenőrizze az exportkönyvtárat.</string>
<string name="export_track_already_exists_msg">A %1$s cím már létezik a célkönyvtárban.</string>
<string name="export_all_do_it_for_all">minden konfliktus esetén</string>
<string name="export_track_errors">Nem exportált sávok</string>
<string name="import_thread_interrupted">Szál megszakadt</string>
<string name="import_unsupported_format">Nem támogatott fájlformátum</string>
<string name="description_default_speed_or_pace">Tevékenységtípus szerint</string>
<string name="export_without_photos">fotók nélkül</string>
<string name="value_float_kilometer_hour">%1$.2f km/h</string>
<string name="voice_speed_lap">%1$s körsebessége</string>
<string name="help_gps_troubleshooting_content"> Mit tehetsz:
\n a) ellenőrizze, hogy a GPS engedélyezve van-e,
\n b) javítsa a GPS-vételt (pl. menjen a szabadba),
\n c) ellenőrizze a <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> alkalmazással, és
\n d) tiltsa le az energiatakarékosságot.
\n
\n Ha mindez nem segít, kérjük, jelentse a hibát. </string>
<string name="export_progress_summary_new_msg">Új pályák exportálása</string>
<string name="generic_completed">Befejezett</string>
<string name="sensor_state_heart_rate_avg">Átlagos pulzusszám</string>
<string name="sensor_state_power_avg">Átlagos teljesítmény</string>
<string name="sensor_not_known">A %1$s érzékelő ismeretlen. Kérjük, ellenőrizze a beállításokat.</string>
<string name="bluetooth_disabled">Kérjük, engedélyezze a Bluetooth funkciót.</string>
<string name="settings_default_trackfileformat">Export/megosztás fájlformátum</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Felvétel közben a statisztika megjelenítése a készülék feloldása nélkül.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Tartsa a képernyőt bekapcsolva</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Felvétel közben tartsa bekapcsolva a képernyőt.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Teljes képernyőn</string>
<string name="sensor_state_cadence_avg">Avg Kadencia</string>
<string name="settings_night_mode_title">UI téma</string>
<string name="settings_night_mode_option_system">Rendszer</string>
<string name="settings_night_mode_option_day">Nap</string>
<string name="settings_night_mode_option_night">Éjszaka</string>
<string name="settings_error_initial_values_restored">Sajnáljuk, de hiba történt. Minden beállítás visszaállt az alapértelmezett értékekre.</string>
<string name="settings_sensor_connecting">Csatlakoztatva %1$s</string>
<string name="settings_sensor_disconnected">Megszakadt a kapcsolat %1$s</string>
<string name="stats_loss">Veszteség</string>
<string name="track_recording_notification_accuracy">Helyszín pontossága: %1$s</string>
<string name="voice_pace_lap">Köridő %1$s</string>
<string name="export_progress_summary_overwrite_msg">Sávok felülírása</string>
<string name="export_progress_summary_skip_msg">Kihagyott sávok</string>
<string name="export_progress_summary_errors_msg">Hibák</string>
<string name="generic_open_track">Nyílt pálya</string>
<string name="generic_show_errors">Hibák megjelenítése</string>
<string name="generic_click_twice_cancel">Kattintson vissza a törléshez</string>
<string name="gps_disabled_msg">GPS letiltva</string>
<string name="gps_fixed_and_ready">GPS rögzítve és készen áll</string>
<string name="import_unable_to_import_file">Nem sikerült importálni a fájlt: %1$s</string>
<string name="import_no_kml_file_found">Nincs doc.kml fájl található a KMZ fájlban</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="import_file_imported">Fájl %1$s importálva</string>
<string name="import_prevent_reimport">Beállítottad, hogy megakadályozd az újraimportálást</string>
<string name="import_progress_summary_ok_msg">Importált fájlok</string>
<string name="import_progress_summary_exists_msg">Már létezik</string>
<string name="menu_insert_gallery_img">Galéria kép beillesztése</string>
<string name="menu_resume_track">Folytassa a pályát</string>
<string name="import_parser_error">Parser hiba: %1$s</string>
<string name="help_data_storage_content"> Az érzékelőadatokat rögzített helyenként tárolják. A pulzusszám-érzékelőre vonatkoztatva ez azt jelenti, hogy csak <i>egy</i> pulzusszámmérés kerül tárolásra helyenként. Ha mozdulatlanul áll vagy beltérben tartózkodik, akkor nem kerülnek rögzítésre az érzékelő adatai (de megjelenítésre kerülnek). </string>
<string name="help_support_content"> Megteheti:
\n a) terjesztheti az OpenTracks hírét,
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">jellemzőket javasolhat</a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">hibákat jelenteni</a>,
\n d) segíthetsz a szoftverfejlesztésben,
\n e) vásárolhat a <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>-ban,
\n f) adományozni, és
\n g) tegyen bármit, ami Ön szerint segít az OpenTracks-nak. </string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Egy hibával befejezve</item>
<item quantity="other">%1$d hibával befejezve</item>
</plurals>
<string name="description_altitude_loss">Magasságveszteség: %1$d m (%2$d ft)</string>
<string name="marker_add_photo_canceled">Törölt, nincs fotójelző hozzáadva</string>
<string name="permission_gps_failed">Az OpenTracks engedélyt igényel a GPS használatához.</string>
<string name="sensor_could_not_scan">Nem sikerült Bluetooth-eszközöket keresni (hiba %1$i).</string>
<string name="settings_recording_show_on_lockscreen_while_recording_title">Statisztikák megjelenítése a záróképernyőn</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Felvétel közben teljes képernyőn jelenik meg.</string>
<string name="settings_prevent_reimport_tracks_title">A pályák újraimportálásának megakadályozása</string>
<string name="aggregated_stats_empty_message">Az első pálya rögzítése az összesített statisztikák megtekintéséhez</string>
<string name="share_image_subject">Szeretnék megosztani veletek egy képet</string>
<string name="share_image_body">Azt hiszem, ez a kép érdekelheti önöket.</string>
<string name="help_recording_title">Hogyan rögzíti az OpenTracks az adatokat\?</string>
<string name="help_track_export_content"> A nyomvonalak KMZ, KML vagy GPX formátumban exportálhatók a Beállításokból. KMZ/KML minden adat; GPX csak a helyszínek. A <i>megosztás ikonon</i> keresztül megosztott pályák KMZ formátumban kerülnek megosztásra. </string>
<string name="help_track_map_content"> Használja a kis térkép ikont.
\n a) (élő frissítések) Telepítse a <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Ez online vagy offline térképeket biztosít a <a href="https://www.openstreetmap.org">OpenStreetMap</a> segítségével.
\n Valójában az OSMDashboard egy különálló alkalmazás, amely csak a megjelenítendő pálya(k) adataihoz fér hozzá (ideiglenesen).
\n b) Egy normál térképalkalmazáson keresztül (pl. <a href="http://osmand.net/">OsmAnd</a> és <a href="https://maps.me/">MAPS.ME</a>). Ezeknek az alkalmazásoknak támogatniuk kell a KMZ fájlformátumot (csak a helyeket és az időbélyegeket). Valójában egy alkalmazásnak támogatnia kell a <i>ACTION_VIEW</i> MIME <i>application/vnd.google-earth.kmz</i> formátumot. </string>
<string name="about_description">Az <i>OpenTracks</i> egy sportkövető alkalmazás, amely teljes mértékben tiszteletben tartja az Ön magánéletét.</string>
<string name="about_history">Az OpenTracks a <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (2016-ban megszűnt) elágazása.</string>
<string name="help_feature_content">* GPS nyomvonalak létrehozása
\n * szívritmus-érzékelő használata (csak Bluetooth LE)
\n * hangbejelentések (idő vagy távolság alapján)
\n * jelölések létrehozása (képekkel együtt)
\n * teljesen offline működik
\n * pályák megjelenítése térképen (lásd alább)</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>
+41 -15
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,8 +13,7 @@ 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.
-->
<resources>
--><resources>
<!-- Activity Type -->
<string name="activity_type_airplane">avião</string>
<string name="activity_type_atv">quadriciclos</string>
@@ -30,7 +30,7 @@ limitations under the License.
<string name="activity_type_driving_bus">ônibus</string>
<string name="activity_type_driving_car">carro</string>
<string name="activity_type_ferry">balsa</string>
<string name="activity_type_frisbee">frisbee</string>
<string name="activity_type_frisbee">frízebee</string>
<string name="activity_type_gliding">asa-delta</string>
<string name="activity_type_hang_gliding">voo livre</string>
<string name="activity_type_helicopter">helicóptero</string>
@@ -75,7 +75,7 @@ limitations under the License.
<string name="activity_type_trail_hiking">caminhada em trilha</string>
<string name="activity_type_trail_running">corrida em trilha</string>
<string name="activity_type_train">treino</string>
<string name="activity_type_ultimate_frisbee">ultimate frisbee</string>
<string name="activity_type_ultimate_frisbee">frízebee final</string>
<string name="activity_type_unknown">desconhecida</string>
<string name="activity_type_wakeboarding">esqui aquático</string>
<string name="activity_type_walking">caminhada</string>
@@ -173,7 +173,7 @@ limitations under the License.
<string name="image_stop">Parar</string>
<string name="image_track">Rota</string>
<!-- Import -->
<string name="import_error">Importação: %1$d de %2$s de %3$s</string>
<string name="import_error">importados %1$d de %2$s desde %3$s</string>
<string name="import_no_directory">%1$s não existe</string>
<string name="import_no_file">Nenhum arquivo para importar. Conecte o dispositivo ao computador e coloque os
arquivos para importação em %1$s.</string>
@@ -185,7 +185,7 @@ limitations under the License.
<!-- Marker -->
<string name="marker_add_error">Não foi possível inserir um marcador. Não há sinal de GPS. Tente novamente.</string>
<string name="marker_add_success">Um marcador foi inserido</string>
<string name="marker_add_canceled">Cancelado. Nenhum marcador foi adicionado.</string>
<string name="marker_add_canceled">Cancelado. Nenhum marcador foi adicionado</string>
<string name="marker_delete_all_confirm_message">Todos os marcadores desta rota serão excluídos permanentemente do dispositivo</string>
<string name="marker_delete_one_confirm_message">O marcador será excluído permanentemente do dispositivo</string>
<string name="marker_delete_one_confirm_title">Excluir marcador?</string>
@@ -255,8 +255,8 @@ limitations under the License.
<string name="settings_recording_location_frequency_summary">%1$s entre os locais gravados</string>
<string name="settings_recording_max_recording_distance_summary">Um novo segmento será criado se a distância entre os dois locais for maior que %1$s</string>
<string name="settings_recording_max_recording_distance_title">Distância máxima</string>
<string name="settings_recording_min_recording_distance_title">Interv. distância grav.</string>
<string name="settings_recording_min_recording_interval_title">Interv. tempo de grav.</string>
<string name="settings_recording_min_recording_distance_title">Interv. distância grav</string>
<string name="settings_recording_min_recording_interval_title">Interv. tempo de grav</string>
<string name="settings_recording_min_required_accuracy_summary">O local será ignorado se a precisão for menor que %1$s</string>
<string name="settings_recording_min_required_accuracy_title">Precisão do GPS</string>
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
@@ -269,7 +269,7 @@ limitations under the License.
<string name="settings_reset_confirm_title">Redefinir agora?</string>
<string name="settings_reset_done">Todas as configurações foram revertidas para os valores padrão</string>
<!-- Settings Sensor -->
<string name="settings_sensor">Sensor</string>
<string name="settings_sensor">Sensores</string>
<string name="settings_sensor_bluetooth">Bluetooth</string>
<string name="settings_sensor_bluetooth_sensor">Sensor Bluetooth</string>
<!-- Settings Stats -->
@@ -278,11 +278,11 @@ limitations under the License.
<string name="settings_stats_units_imperial">Imperial (milhas, pés)</string>
<string name="settings_stats_units_metric">Métrica (km, m)</string>
<string name="settings_stats_units_title">Unidades preferenciais</string>
<!-- Share Track -->
<string name="share_track_share_file">Compartilhar arquivo da trilha</string>
<string name="share_track_share_file_body">Achei que você pudesse se interessar por essa trilha.\n\nPara visualizá-la, use o Google Earth ou outro
software de mapas.\n\n%1$s</string>
<string name="share_track_share_file_body">Achei que você pudesse se interessar por essa trilha.
\n
\n%1$s</string>
<string name="share_track_subject">Eu gostaria de compartilhar uma trilha com você</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Ritmo médio de movimentação</string>
@@ -296,7 +296,7 @@ limitations under the License.
<string name="stats_latitude">Latitude</string>
<string name="stats_longitude">Longitude</string>
<string name="stats_max">Máx.</string>
<string name="stats_max_speed">Velocidade máx.</string>
<string name="stats_max_speed">Velocidade máx</string>
<string name="stats_min">Mín.</string>
<string name="stats_moving_time">Tempo decorrido</string>
<string name="stats_pace">Ritmo</string>
@@ -464,4 +464,30 @@ limitations under the License.
<string name="waypoint_type_waterfall">Cascata</string>
<string name="waypoint_type_water_fountain">fonte de água</string>
<string name="waypoint_type_waypoint">ponto de referência</string>
</resources>
<string name="help_track_map_title">Como mostrar uma pista num mapa\?</string>
<string name="help_feature_content">* criar pistas GPS
\n * utilizar um sensor do ritmo cardíaco (apenas Bluetooth LE)
\n * anúncios de voz (por tempo ou distância)
\n * criar marcadores (incl. imagens)
\n * funciona completamente offline
\n * mostrar pistas num mapa (ver abaixo)</string>
<string name="help_track_map_content"> Utilize o pequeno ícone do mapa.
\n a) (actualizações ao vivo) Instalar <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Fornece mapas online ou offline através de <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n Na verdade, o OSMDashboard é uma aplicação separada que só pode aceder (temporariamente) aos dados da(s) faixa(s) que vão ser mostrados.
\n b) Através de uma aplicação de mapa normal (por exemplo, <a href="http://osmand.net/">OsmAnd</a> e <a href="https://maps.me/">MAPS.ME</a>). Estas aplicações precisam de suportar o formato de ficheiro KMZ (apenas locais e timestamps). De facto, uma aplicação precisa de suportar <i>ACTION_VIEW</i> com MIME <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_gps_troubleshooting_content"> O que se pode fazer:
\n a) verificar se o GPS está activado,
\n b) melhorar a recepção GPS (por exemplo, ir para o exterior),
\n c) verificar com <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, e
\n d) desactivar a poupança de energia.
\n
\n Se tudo isto não ajudar, por favor, informe um erro. </string>
<string name="help_support_content"> Pode:
\n a) espalhar a palavra sobre OpenTracks,
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">propose features</a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">reportar bugs</a>,
\n d) ajuda no desenvolvimento de software,
\n e) comprar em <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
\n f) doar, e
\n g) fazer o que achar que ajuda a OpenTracks. </string>
</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>
+4
View File
@@ -462,4 +462,8 @@ limitations under the License.
<string name="waypoint_type_water_fountain">đài phun nước</string>
<string name="waypoint_type_waypoint">điểm tham chiếu</string>
<string name="about_preference_title">Về OpenTracks</string>
<string name="about_version_name">Tên phiên bản: %1$s</string>
<string name="about_version_code">Mã phiên bản: %1$d</string>
<string name="about_libraries">Thư viện</string>
<string name="help_feature_title">OpenTracks có những tính năng sau đây:</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>