Compare commits

...

61 Commits

Author SHA1 Message Date
Dennis Guse 183b47a1ea Release: v3.23.0 2021-12-21 22:48:35 +01:00
mondstern bc84e8cb9a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-12-21 20:53:19 +01:00
mondstern fb9e67ae85 Translated using Weblate (Estonian)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-12-21 20:53:19 +01:00
mondstern 8877f3fe14 Translated using Weblate (Czech)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-12-21 20:53:18 +01:00
mondstern 963a37b877 Translated using Weblate (Italian)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-12-21 20:53:18 +01:00
Dennis Guse 3be1348df1 Merge pull request #1047 from OpenTracksApp/movingTime#1046
Restore moving time implementation via idle/non-idle TrackPoints.
Fixes #1046.
2021-12-21 20:52:59 +01:00
Dennis Guse 530967f4f6 Moving time: disable test on API23. 2021-12-21 17:46:23 +01:00
Dennis Guse 25e590da02 Moving time: re-implement idle/non-idle TrackPoints.
Simplify computation of TrackStatistics.

Part of #1046.
2021-12-19 22:01:35 +01:00
Dennis Guse 0be4b25508 Moving time: fix test.
Part of #1046.
2021-12-19 21:56:53 +01:00
Dennis Guse 4c7d9384fb Revert "Only record a TrackPoint if the traveled distance is greater than recording distance interval."
This reverts commit 08ba7d50c7.

Fix #1046.
2021-12-19 21:56:53 +01:00
Dennis Guse 101701c6ff CI: use Android 23 and 30. 2021-12-19 21:56:39 +01:00
Allan Nordhøy 8f003e2f7f Fastlane: reworked description 2021-12-19 21:54:55 +01:00
Dennis Guse 03d825d2ec Cleanup: reference resources manually. 2021-12-19 21:54:13 +01:00
dependabot[bot] 0977d34889 Bump mockito-android from 4.1.0 to 4.2.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-17 07:41:26 +01:00
Quang Trung 6dee9884ff Translated using Weblate (Vietnamese)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-12-15 21:39:24 +01:00
ThePPK 7b44b42def Translated using Weblate (Polish)
Currently translated at 92.2% (71 of 77 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/pl/
2021-12-15 21:39:24 +01:00
ThePPK ad712f28c6 Translated using Weblate (Polish)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-12-15 21:39:24 +01:00
Peter Storch b287967bc4 fix persistable directory permissions 2021-12-15 21:39:10 +01:00
dependabot[bot] 29c72802aa Bump orchestrator from 1.4.0 to 1.4.1
Bumps orchestrator from 1.4.0 to 1.4.1.

---
updated-dependencies:
- dependency-name: androidx.test:orchestrator
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-14 08:07:06 +01:00
FTno f5241e8046 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-12-13 18:21:06 +01:00
Xosé M 1e720b2fa7 Translated using Weblate (Galician)
Currently translated at 100.0% (466 of 466 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-12-13 18:21:06 +01:00
Román Martínez 19382f19c4 Aggregated stats for only selected tracks. 2021-12-12 21:38:08 +01:00
Dennis Guse 14b8ab21db Add Github funding 2021-12-09 17:25:33 +01:00
dependabot[bot] 00b0e1face Bump gradle from 7.0.3 to 7.0.4
Bumps gradle from 7.0.3 to 7.0.4.

---
updated-dependencies:
- dependency-name: com.android.tools.build:gradle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-09 08:15:48 +01:00
Román Martínez 7f9057c519 Added profiles for custom layouts.
Users can add severals custom layouts and select one of them in recording fragment. The last selected layout is the one that will be use in the next recording.
Fixes #938.
2021-12-07 18:07:25 +01:00
Dennis Guse 02cec48ae4 Merge pull request #1036 from OpenTracksApp/npe#1035
Fix NPE #1035
2021-12-04 22:23:27 +01:00
Dennis Guse 0d45efdf07 Cleanup. 2021-12-04 21:47:43 +01:00
Dennis Guse b6d935cf34 Bugfix: prevent NPE as handler should not be executed anymore.
Fixes #1035.
2021-12-04 21:47:43 +01:00
mondstern 3228f58187 Translated using Weblate (Estonian)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-12-03 19:06:43 +01:00
Dennis Guse 1e27542aea Bugfix: free memory in onDestroy(). 2021-11-29 21:33:12 +01:00
mondstern fd7892bdf7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-11-29 08:15:25 +01:00
mondstern 2ae8a753c9 Translated using Weblate (Estonian)
Currently translated at 99.1% (453 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/et/
2021-11-29 08:15:25 +01:00
Peter Storch bb7efaa87c fix #1025 exportdir failed to create document
fix #254 illegal filename characters
2021-11-29 08:15:08 +01:00
mondstern 2eab7425f2 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-11-26 23:06:46 +01:00
FTno a19bbd6153 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-26 23:06:45 +01:00
mondstern b523b8af9e Translated using Weblate (French)
Currently translated at 92.2% (71 of 77 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/fr/
2021-11-24 20:06:44 +01:00
mondstern e9a71a10f6 Translated using Weblate (Vietnamese)
Currently translated at 97.1% (444 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-11-24 20:06:43 +01:00
silkevicious a0d69b589b Translated using Weblate (Italian)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-11-24 20:06:42 +01:00
mondstern 71af495835 Translated using Weblate (French)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-11-24 20:06:41 +01:00
mondstern ed4fb30b35 Translated using Weblate (Czech)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-11-24 20:06:40 +01:00
mondstern 7f1c014bf8 Translated using Weblate (Galician)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-11-24 00:06:57 +01:00
FTno affa72fad7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (457 of 457 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-22 18:24:14 +01:00
FTno 14a14cd91f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-22 18:24:14 +01:00
dependabot[bot] ff89d2c5ec Bump mockito-android from 4.0.0 to 4.1.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 18:24:07 +01:00
Peter Storch 54f1690478 Change versionCode for nightlies to commit count 2021-11-20 23:25:17 +01:00
Dennis Guse 681058c3e6 i18n: move translatable settings strings into strings.xml 2021-11-20 22:05:08 +01:00
Dennis Guse d93e025963 BLE sensors: clear data when disconnected.
Part of #1027.
2021-11-20 19:43:38 +01:00
Dennis Guse 1f0a99c49a Recording: ignore TrackPoints that contain sensor data and do not fulfill recording distance interval.
Follow-up to 08ba7d50c7

Part of #1027.
2021-11-20 19:43:38 +01:00
Román Martínez 976d4f848f Bugfixed: illegal state exception crashed fixed. Fixes #1022. 2021-11-18 17:15:53 +01:00
dependabot[bot] 540a3340b3 Bump appcompat from 1.3.1 to 1.4.0
Bumps appcompat from 1.3.1 to 1.4.0.

---
updated-dependencies:
- dependency-name: androidx.appcompat:appcompat
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-18 12:06:56 +01:00
mondstern 2b4c68f3ec Translated using Weblate (Vietnamese)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-11-16 22:42:36 +01:00
mondstern 90d55e419e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-11-16 22:42:36 +01:00
mondstern ac31f4fd6a Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-11-16 22:42:36 +01:00
mondstern 32a75bb479 Translated using Weblate (Galician)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-11-16 22:42:36 +01:00
mondstern d32f1e05ec Translated using Weblate (French)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-11-16 22:42:36 +01:00
mondstern defd6aed85 Translated using Weblate (Spanish)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-11-16 22:42:36 +01:00
vsevjednom-cz 7463dc703c Translated using Weblate (Czech)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-11-16 22:42:36 +01:00
mondstern 2ab04da936 Translated using Weblate (Czech)
Currently translated at 100.0% (439 of 439 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-11-16 22:42:36 +01:00
Dennis Guse 6b84d3b9ba Nightly build: icon has now a black background.
Fixes #952.
2021-11-16 22:42:30 +01:00
vsevjednom-cz 78b57cdc67 Voice announcements: normalize non-TTS audio replacement 2021-11-15 22:40:30 +01:00
Dennis Guse 35de5b6b26 Cleanup: BLE cycling speed/distance sensor parsing should be the other way around./
Was seemingly still working...
2021-11-15 18:53:14 +01:00
78 changed files with 2547 additions and 456 deletions
+1
View File
@@ -1,4 +1,5 @@
# These are supported funding model platforms
liberapay: OpenTracks
github: [rgmf, pstorch, dennisguse]
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
+2 -1
View File
@@ -7,8 +7,9 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
api-level: [ 23, 26 ] #TODO Re-enable 29
api-level: [ 23, 30 ]
target: [ default ]
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v2.4.0
+18 -11
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.android.tools.build:gradle:7.0.4'
}
}
@@ -30,10 +30,17 @@ def getVersionName = { ->
}
}
def versionCodeFromDate = { ->
def date = new Date()
def formattedDate = date.format('yyyyMMdd')
return Integer.valueOf(formattedDate)
def getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', 'HEAD', '--count'
standardOutput = stdout
}
return Integer.valueOf(stdout.toString().trim())
} catch (ignored) {
return null
}
}
android {
@@ -56,8 +63,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4519
versionName "v3.22.0"
versionCode 4580
versionName "v3.23.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -105,7 +112,7 @@ android {
if (variant.buildType.name == 'nightly') {
variant.outputs.all {
setVersionCodeOverride(versionCodeFromDate())
setVersionCodeOverride(getVersionCode())
setVersionNameOverride(getVersionName())
outputFileName = "${applicationId}_${variant.versionCode}.apk"
}
@@ -116,7 +123,7 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.4.0'
@@ -127,7 +134,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.0.0'
androidTestImplementation 'org.mockito:mockito-android:4.2.0'
androidTestUtil 'androidx.test:orchestrator:1.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
}
@@ -0,0 +1,10 @@
v3.23.0: OpenTracks
Changes:
- Added UI profiles
- Aggregated statistics for selected tracks
- Re-implemented idle/non-idle TrackPoints as moving time stopped working (introduced in v3.22.0)
Bugfixes:
- BLE sensors: clear data on disconnect
- Automatic export needs to persist permission
@@ -1,11 +1,22 @@
OpenTracks is a sport tracking application that completely respects your privacy.
A sport tracking buddy that respects your privacy.
Features:
* Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking
* Altitude gain and loss: via barometric sensor
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84
* Voice announcements
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
* Export data: export tracks either as KMZ (incl. photos), KML, or GPX
* No Internet access: Internet is not used
Sport and outdoor activities grants happiness, skill and confidence.
Value your health by keeping track on your training.
Records as you go running or walking, and gives you a bike computer with a bigger screen for cycling.
Mark interesting locations along your way with pictures.
Keep recorded statistics in great detail for analysis.
Share only the data you want others to have.
* Voice announcements.
* Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling).
* Altitude gain and loss: via barometric sensor.
* Altitude shown in EGM2008 (above mean sea level); exported as WGS84.
* Export data as tracks either as KMZ (incl. photos), KML, or GPX.
* No Internet access or extra permissions.
* Dark and light theme, respecting system settings.
* No ads.
Libre software / Free Software / Open Source
meaning you can use, study, change and share the source code.
Licensed Apache 2.0
@@ -1 +1 @@
A sport tracker that completely respects your privacy.
A sport tracker buddy that respects your privacy.
@@ -0,0 +1,8 @@
v3.19.1 : OpenTracks
Changements :
- Traductions mises à jour.
Correction de bogue :
- Correction du bug de l'exportation de toutes les pistes.
- Correction du bogue "Reprendre une piste".
@@ -0,0 +1,7 @@
v3.20.0 : OpenTracks
Changements :
- Pendant l'enregistrement : l'interface des statistiques est maintenant configurable
Correction d'un bug :
- Le changement de nom de piste/catégorie pendant l'enregistrement était ignoré (introduit dans la v3.19.0)
@@ -0,0 +1,6 @@
v3.20.1 : OpenTracks
Changements :
- Les annonces peuvent maintenant être utilisées à la fois par distance et par temps.
- Les annonces émettent un bip si aucun logiciel de synthèse vocale n'est installé.
- Suppression de la fréquence de localisation GPS adaptative
@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Zmiany:
- "Pokaż na mapie"- Funkcjonalość wraz z KML zamiast KMZ
- Komunikaty głosowe nie podają nazwy aplikacji
- Podczas śledzenia: Zmiana wysokości aktualizowana co sekundę
- Przetłumaczone opisy w sklepach aplikacji
Naprawione błędy:
- Naprawiono nieprawidłowe tłumaczenia wiadomości głosowych
Developer:
- Uprawnienia Android12 (BLUETOOTH_SCAN)
- Uprawnienia Android12 (ACCESS_COARSE_LOCATION)
- Zastosowanie TrackPoints (testowane)
- Usunięcie niepotrzebnych tłumaczeń
@@ -0,0 +1,11 @@
v3.22.0: OpenTracks
Zmiany:
- Lokalizacja zapisywana tylko wtedy gdy pokonany dystans jest większy od minimalnego przedziału śledzenia dystansu
(usunięto funkcjonalność z wykrywania bezczynności)
- UI może zostać skonfigurowane do wyświetlania zegara
- Prędkość ruchu bez oporu (prędkość jałowa) jest teraz konfigurowalna
Poprawki:
- Przedziały: awaria, gdy zapisano zwiększenie wysokości, ale nie spadek
- Podczas nagrywania: zapobieganie awariom podczas wyłączania komunikatów głosowych
@@ -3,9 +3,9 @@ OpenTracks jest aplikacją do śledzenia sportu, która całkowicie szanuje Twoj
Cechy:
* Śledzenie sportu: śledź swoją aktywność sportową i outdoorową.
* Zdjęcia i markery: zaznacz interesujące lokalizacje podczas śledzenia.
* Przyrost i utrata wysokości: poprzez czujnik barometryczny
* Zmiana wysokości: poprzez czujnik barometryczny
* Wysokość pokazana w EGM2008 (powyżej średniego poziomu morza); wyeksportowana jako WGS84
* Komunikaty głosowe
* Obsługa czujników Bluetooth LE: tętno, prędkość i dystans (kolarstwo), kadencja (kolarstwo) i miernik mocy (kolarstwo)
* Eksport danych: eksport tras jako KMZ (w tym zdjęcia), KML lub GPX
* Brak dostępu do Internetu: Internet nie jest używany
* Dostępu do Internetu: Internet nie jest używany
@@ -0,0 +1,154 @@
package de.dennisguse.opentracks.content.data;
import android.content.Context;
import android.content.res.Resources;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@RunWith(AndroidJUnit4.class)
public class LayoutTest extends TestCase {
private final Context context = ApplicationProvider.getApplicationContext();
private final Resources resources = ApplicationProvider.getApplicationContext().getResources();
@Test
public void testFromCsv() {
// given a csv line
String csv = "running;2;" + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1,0;" + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + context.getString(R.string.stats_custom_layout_altitude_key) + ",0,1,0;" + context.getString(R.string.stats_custom_layout_gain_key) + ",0,0,0;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv, resources);
List<DataField> dataFieldList = layout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "running");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(dataFieldList.size(), 4);
assertEquals(dataFieldList.get(0).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
assertEquals(dataFieldList.get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
assertEquals(dataFieldList.get(2).getKey(), context.getString(R.string.stats_custom_layout_altitude_key));
assertEquals(dataFieldList.get(3).getKey(), context.getString(R.string.stats_custom_layout_gain_key));
assertEquals(dataFieldList.get(0).getTitle(), context.getString(R.string.stats_speed));
assertEquals(dataFieldList.get(1).getTitle(), context.getString(R.string.stats_distance));
assertEquals(dataFieldList.get(2).getTitle(), context.getString(R.string.stats_altitude));
assertEquals(dataFieldList.get(3).getTitle(), context.getString(R.string.stats_gain));
assertTrue(dataFieldList.get(0).isVisible());
assertTrue(dataFieldList.get(0).isPrimary());
assertFalse(dataFieldList.get(0).isWide());
assertTrue(dataFieldList.get(1).isVisible());
assertFalse(dataFieldList.get(1).isPrimary());
assertFalse(dataFieldList.get(1).isWide());
assertFalse(dataFieldList.get(2).isVisible());
assertTrue(dataFieldList.get(2).isPrimary());
assertFalse(dataFieldList.get(2).isWide());
assertFalse(dataFieldList.get(3).isVisible());
assertFalse(dataFieldList.get(3).isPrimary());
assertFalse(dataFieldList.get(3).isWide());
}
@Test
public void testFromCsv_Wrong1() {
// given a csv line without number of columns
String csv1 = "Layout Name;speed,1,1;distance,0,0;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(layout.getFields().size(), 0);
}
@Test
public void testFromCsv_Wrong2() {
// given a csv line without any field
String csv1 = "Layout Name;2;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName());
assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
assertEquals(layout.getFields().size(), 0);
}
@Test
public void testFromCsv_Wrong3() {
// given a csv line with wrong fields description
String csv1 = "Layout Name;2;speed,distance,total time;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "Layout Name");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().size(), 0);
}
@Test
public void testFromCsv_Wrong4() {
// given a csv line with the first field ok but not the others
String csv1 = "Layout Name;2;speed,1,0;distance;";
// when create a layout from CSV line
Layout layout = Layout.fromCsv(csv1, resources);
List<DataField> dataFieldList = layout.getFields();
// then layout and data fields are built correctly
assertEquals(layout.getName(), "Layout Name");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().size(), 1);
}
@Test
public void testToCsv() {
// given a layout's object
Layout layout = new Layout("Test Layout", 2);
layout.addField("key1", "Title 1", false, false, false);
layout.addField("key2", "Title 2", false, true, false);
layout.addField("key3", "Title 3", true, false, false);
layout.addField("key4", "Title 4", true, true, false);
layout.addField("key5", "Title 5", true, true, true);
// when converts it to CSV
String csv = layout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
}
@Test
public void testToCsv_columnsByDefault() {
// given a layout's object
Layout layout = new Layout("Test Layout");
layout.addField("key1", "Title 1", false, false, false);
layout.addField("key2", "Title 2", false, true, false);
layout.addField("key3", "Title 3", true, false, false);
layout.addField("key4", "Title 4", true, true, false);
layout.addField("key5", "Title 5", true, true, true);
// when converts it to CSV
String csv = layout.toCsv();
// then csv is well built
assertEquals(csv, "Test Layout;" + PreferencesUtils.getLayoutColumnsByDefault() + ";key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
}
}
@@ -124,7 +124,7 @@ public class ExportImportTest {
trackPointCreator.setClock(Clock.fixed(Instant.parse("2020-02-02T02:02:02Z"), ZoneId.of("CET")));
trackId = service.startNewTrack();
Distance sensorDistance = hasSensorDistance ? Distance.of(5) : null;
Distance sensorDistance = hasSensorDistance ? Distance.of(10) : null; // recording distance interval
sendLocation(trackPointCreator, Instant.parse("2020-02-02T02:02:03Z"), 3, 14, 10, 15, 10, 1, 66, 3, 50, sensorDistance);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
@@ -218,12 +218,12 @@ public class ExportImportTest {
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(30), importedTrackStatistics.getTotalDistance());
assertEquals(Distance.of(60), importedTrackStatistics.getTotalDistance());
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(3.75), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(7.5), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(Speed.of(7.5), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageMovingSpeed());
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -312,16 +312,16 @@ public class ExportImportTest {
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(20), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(4), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(Distance.of(30), importedTrackStatistics.getTotalDistance());
assertEquals(Distance.of(60), importedTrackStatistics.getTotalDistance());
// Speed
assertEquals(Speed.of(15), importedTrackStatistics.getMaxSpeed());
assertEquals(Speed.of(1.5), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(1.5789473684210527), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageSpeed());
assertEquals(Speed.of(15), importedTrackStatistics.getAverageMovingSpeed());
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -83,14 +83,14 @@ public class TrackPointAssert {
try {
Assert.assertEquals(expected.size(), actual.size());
} catch (AssertionError e) {
throw new AssertionError("Expected: " + expected + "\n actual: " + actual);
throw new AssertionError("Size difference; expected: " + expected.size() + "; actual: " + actual.size() + "\nExpected: " + expected + "\n actual: " + actual);
}
for (int i = 0; i < expected.size(); i++) {
try {
assertEquals(expected.get(i), actual.get(i));
} catch (AssertionError e) {
throw new AssertionError("Expected: " + expected.get(i) + "\n actual: " + actual.get(i), e);
throw new AssertionError("Expected: " + i + " to be " + expected.get(i) + "\n actual: " + actual.get(i), e);
}
}
Assert.assertEquals(expected.size(), actual.size());
@@ -64,8 +64,8 @@ import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for the track recording service.
@@ -451,10 +451,7 @@ public class TrackRecordingServiceTest {
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
}
/**
* Inserts a location and waits for 200ms.
*/
private static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) {
static void newTrackPoint(TrackRecordingService trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(time))
.setLongitude(longitude)
.setLatitude(latitude)
@@ -20,14 +20,11 @@ import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.internal.debugging.LocationImpl;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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;
@@ -190,7 +187,7 @@ public class TrackRecordingServiceTestLocation {
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 3, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 4, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 6, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 6, 15);
service.endCurrentTrack();
@@ -210,7 +207,7 @@ public class TrackRecordingServiceTestLocation {
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.0002)
.setLatitude(45.0005)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
@@ -220,9 +217,6 @@ public class TrackRecordingServiceTestLocation {
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
), trackPoints);
}
@MediumTest
@@ -257,6 +251,13 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
@@ -274,8 +275,8 @@ public class TrackRecordingServiceTestLocation {
// when
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0); // will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0); // will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 15);
@@ -296,6 +297,27 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(6))
.setSpeed(Speed.of(15))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
@@ -350,38 +372,6 @@ public class TrackRecordingServiceTestLocation {
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate_bpm(5f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
@@ -462,12 +452,20 @@ public class TrackRecordingServiceTestLocation {
.setHorizontalAccuracy(Distance.of(2))
.setSpeed(Speed.of(5))
.setSensorDistance(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(4))
.setSpeed(Speed.of(5))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setSensorDistance(Distance.of(11))
.setSpeed(Speed.of(5))
.setAltitudeGain(12f)
.setAltitudeLoss(12f)
.setSensorDistance(Distance.of(4))
.setAltitudeGain(6f)
.setAltitudeLoss(6f)
.setSensorDistance(Distance.of(2))
), trackPoints);
}
@@ -511,6 +509,13 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.1)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(3))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, null)
.setLatitude(45.2)
@@ -519,6 +524,13 @@ public class TrackRecordingServiceTestLocation {
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT, null)
.setLatitude(45.2)
.setLongitude(35)
.setHorizontalAccuracy(Distance.of(5))
.setSpeed(Speed.of(0))
.setAltitudeGain(0f)
.setAltitudeLoss(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, null)
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
@@ -0,0 +1,198 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.services.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests resulting TrackStatistics.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestStatistics {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingService service;
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
@Override
public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(0f);
trackPoint.setAltitudeLoss(0f);
}
};
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
tearDown();
// Let's use default values.
PreferencesUtils.clear();
service = ((TrackRecordingService.Binder) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)))
.getService();
service.getTrackPointCreator().stopGPS();
}
@After
public void tearDown() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setClock(Clock.systemUTC());
}
/**
* Moving time should increase if the previous and current TrackPoint have speed > threshold by the timeDiff(previousTrackPoint, currentTrackPoint).
*/
@MediumTest
@Test
public void movingtime_with_pauses() {
Assume.assumeTrue(
"Test fails on API23; reproducible on CI and some machines.",
Build.VERSION.SDK_INT > 23
);
// given
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()));
Track.Id trackId = service.startNewTrack();
service.getTrackPointCreator().setAltitudeSumManager(altitudeSumManager);
Function<Integer, Void> assertMovingTime = expected -> {
Duration actual = contentProviderUtils.getTrack(trackId).getTrackStatistics().getMovingTime();
assertEquals(Duration.ofSeconds(expected), actual);
return null;
};
Function<Integer, Void> assertTotalTime = expected -> {
Duration actual = contentProviderUtils.getTrack(trackId).getTrackStatistics().getTotalTime();
assertEquals(Duration.ofSeconds(expected), actual);
return null;
};
// when / then
int movingtime_s = 0;
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15, 5 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 2, 15, 6 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 2, 15, (long) (6.5 * 60000));
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 2, 15, 7 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, 8 * 60000);
movingtime_s += 3 * 60;
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 9 * 60000); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 10 * 60000); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 0, 11 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, 13 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 2, 15, (long) (13.5 * 60000)); //will be ignored
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 2, 15, 14 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0006, 35.0, 2, 15, 15 * 60000);
movingtime_s += 2 * 60;
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(15 * 60);
TrackRecordingServiceTest.newTrackPoint(service, 45.0006, 35.0, 2, 0, 16 * 60000); //will be ignored
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0015, 35.0, 2, 0, 17 * 60000);
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 15, 18 * 60000);
assertMovingTime.apply(movingtime_s);
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 0, 19 * 60000); //TODO we could ignore this TrackPoint
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(19 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(40 * 60), ZoneId.systemDefault()));
assertMovingTime.apply(movingtime_s);
service.pauseCurrentTrack();
assertTotalTime.apply(40 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(41 * 60), ZoneId.systemDefault()));
service.resumeCurrentTrack();
TrackRecordingServiceTest.newTrackPoint(service, 45.0016, 35.0, 2, 15, 42 * 60000);
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(41 * 60);
service.getTrackPointCreator().setClock(Clock.fixed(Instant.ofEpochSecond(50 * 60), ZoneId.systemDefault()));
service.endCurrentTrack();
assertMovingTime.apply(movingtime_s);
assertTotalTime.apply(49 * 60);
// then
assertFalse(service.isRecording());
TrackStatistics trackStatistics = contentProviderUtils.getTrack(trackId).getTrackStatistics();
List<TrackPoint> trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
assertEquals(20, trackPoints.size());
assertEquals(Duration.ofMinutes(49), trackStatistics.getTotalTime());
}
}
@@ -0,0 +1,118 @@
package de.dennisguse.opentracks.settings;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Layout;
@RunWith(AndroidJUnit4.class)
public class PreferencesOpenHelperTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final Resources resources = ApplicationProvider.getApplicationContext().getResources();
@Test
public void test_upgradeFrom0To1_withoutStatsCustomLayouts() {
// given the version 0
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(context.getString(R.string.stats_custom_layouts_key));
editor.putInt(context.getString(R.string.prefs_last_version_key), 0);
editor.commit();
// when update to version 1
PreferencesOpenHelper.newInstance(1).check();
// then there should be one layout with old custom layout that has the new CSV value.
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv());
}
@Test
public void test_upgradeFrom1To2_withOldVersion() {
// given the version 1
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
String oldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";distance,1,1,0;speed,1,1,0;";
editor.putString(context.getString(R.string.stats_custom_layouts_key), oldCustomLayoutCsv);
editor.putInt(context.getString(R.string.prefs_last_version_key), 1);
editor.commit();
// when update to version 2
PreferencesOpenHelper.newInstance(2).check();
String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";"
+ PreferencesUtils.getLayoutColumnsByDefault() + ";distance,1,1,0;speed,1,1,0;";
// then there should be one layout with old custom layout that has the new CSV value.
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
}
@Test
public void test_upgradeFrom1To2_withNewVersion() {
// given the version 1
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
String oldCustomLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;";
editor.putString(context.getString(R.string.stats_custom_layouts_key), oldCustomLayoutCsv);
editor.putInt(context.getString(R.string.prefs_last_version_key), 1);
editor.commit();
// when update to version 2
PreferencesOpenHelper.newInstance(2).check();
String updatedOldCustomLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;";
// then there should be one layout with old custom layout that has the new CSV value.
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
assertNotNull(layouts);
assertEquals(layouts.size(), 1);
assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
}
@Test
public void test_downgrade() {
// given version 2
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
String customLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;";
editor.putString(context.getString(R.string.stats_custom_layouts_key), customLayoutCsv);
editor.putInt(context.getString(R.string.prefs_last_version_key), 2);
editor.commit();
// when downgrade to version 1
PreferencesOpenHelper.newInstance(1).check();
// then custom layout should be equals to default layout.
Layout defaultLayout = Layout.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
List<Layout> customLayout = PreferencesUtils.getAllCustomLayouts();
assertEquals(customLayout.size(), 1);
assertEquals(defaultLayout.toCsv(), customLayout.get(0).toCsv());
}
}
@@ -1,5 +1,8 @@
package de.dennisguse.opentracks.stats;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
@@ -16,9 +19,6 @@ import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest {
@@ -81,18 +81,18 @@ public class TrackStatisticsUpdaterTest {
// then
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(99.58, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(14), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(7), statistics.getMovingTime());
assertEquals(128.03, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofSeconds(13), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(9), statistics.getMovingTime());
assertEquals(2.5, statistics.getMinAltitude(), 0.01);
assertEquals(27.5, statistics.getMaxAltitude(), 0.01);
assertEquals(28.75, statistics.getMaxAltitude(), 0.01);
assertEquals(36, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(36, statistics.getTotalAltitudeLoss(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(7.11, statistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(9.84, statistics.getAverageSpeed().toMPS(), 0.01);
}
@Test
@@ -141,9 +141,9 @@ public class TrackStatisticsUpdaterTest {
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, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp2.setSpeed(Speed.of(5f));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp2.setSpeed(Speed.of(5f));
tp4.setSpeed(Speed.of(5f));
tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(Distance.of(10f));
@@ -200,9 +200,12 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
@@ -248,8 +251,11 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
tp4.setSpeed(Speed.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -43,40 +43,40 @@ public class BluetoothUtilsTest {
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertNull(sensor.getDistanceSpeed());
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
}
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
assertNull(sensor.getCadence());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
}
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getCadence().getCrankRevolutionsCount());
}
@Test
@@ -0,0 +1,93 @@
package de.dennisguse.opentracks.util;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
@RunWith(AndroidJUnit4.class)
public class CsvLayoutUtilsTest extends TestCase {
@Test
public void testGetCsvLineParts() {
// given
String csvLineOk = "Layout Name;2;distance,1,1;speed,1,1;";
String csvLineWrong1 = "Layout Name;distance,1,1;speed,1,1;";
String csvLineWrong2 = "Layout Name;1;";
// when
List<String> partsOk = CsvLayoutUtils.getCsvLineParts(csvLineOk);
List<String> partsWrong1 = CsvLayoutUtils.getCsvLineParts(csvLineWrong1);
List<String> partsWrong2 = CsvLayoutUtils.getCsvLineParts(csvLineWrong2);
// then
assertNotNull(partsOk);
assertEquals(partsOk.size(), 4);
assertEquals(partsOk.get(0), "Layout Name");
assertEquals(partsOk.get(1), "2");
assertEquals(partsOk.get(2), "distance,1,1");
assertEquals(partsOk.get(3), "speed,1,1");
assertNull(partsWrong1);
assertNull(partsWrong2);
}
@Test
public void testGetCsvFieldParts() {
// given
String csvFieldOk1 = "distance,0,0";
String csvFieldOk2 = "speed,0,1";
String csvFieldOk3 = "time,1,0";
String csvFieldOk4 = "moving time,1,1";
String csvFieldOk5 = ";moving time,1,1";
String csvLineWrong1 = "";
String csvLineWrong2 = "distance,speed,time";
String csvLineWrong3 = "distance,0,1o";
String csvLineWrong4 = "distance,0z,1";
String csvLineWrong5 = "distance";
String csvLineWrong6 = "distance,0";
String csvLineWrong7 = "distance,1,1;";
// when
String[] ok1 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk1);
String[] ok2 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk2);
String[] ok3 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk3);
String[] ok4 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk4);
String[] ok5 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk5);
String[] wrong1 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong1);
String[] wrong2 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong2);
String[] wrong3 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong3);
String[] wrong4 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong4);
String[] wrong5 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong5);
String[] wrong6 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong6);
String[] wrong7 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong7);
// then
assertNotNull(ok1);
assertNotNull(ok2);
assertNotNull(ok3);
assertNotNull(ok4);
assertNotNull(ok5);
assertFieldOk(ok1, "distance", "0", "0");
assertFieldOk(ok2, "speed", "0", "1");
assertFieldOk(ok3, "time", "1", "0");
assertFieldOk(ok4, "moving time", "1", "1");
assertFieldOk(ok5, ";moving time", "1", "1");
assertNull(wrong1);
assertNull(wrong2);
assertNull(wrong3);
assertNull(wrong4);
assertNull(wrong5);
assertNull(wrong6);
assertNull(wrong7);
}
private void assertFieldOk(String[] ok, String name, String val1, String val2) {
assertEquals(ok[0], name);
assertEquals(ok[1], val1);
assertEquals(ok[2], val2);
}
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
@@ -21,10 +22,14 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidJUnit4.class)
public class PreferencesUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final Resources resources = ApplicationProvider.getApplicationContext().getResources();
@Test
public void ExportTrackFileFormat_ok() {
@@ -72,6 +77,24 @@ public class PreferencesUtilsTest {
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
}
@Test
public void testGetAllCustomLayouts_default() {
// given
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.clear();
editor.commit();
// when
List<Layout> layouts = PreferencesUtils.getAllCustomLayouts();
// then
assertEquals(layouts.size(), 1);
assertTrue(layouts.get(0).getFields().size() > 0);
assertEquals(layouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(layouts.get(0).getFields().stream().anyMatch(DataField::isVisible));
}
@Test
public void testGetCustomLayout_default() {
// given
@@ -85,7 +108,7 @@ public class PreferencesUtilsTest {
// then
assertTrue(layout.getFields().size() > 0);
assertEquals(layout.getProfile(), context.getString(R.string.default_activity_default));
assertEquals(layout.getName(), context.getString(R.string.stats_custom_layout_default_layout));
assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible));
}
@@ -95,8 +118,8 @@ public class PreferencesUtilsTest {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"run;"
context.getString(R.string.stats_custom_layouts_key),
"run;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
@@ -108,7 +131,8 @@ public class PreferencesUtilsTest {
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "run");
assertEquals(layout.getName(), "run");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
@@ -133,8 +157,8 @@ public class PreferencesUtilsTest {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(
context.getString(R.string.stats_custom_layout_fields_key),
"walking;"
context.getString(R.string.stats_custom_layouts_key),
"walking;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_coordinates_key) + ",0,1;"
@@ -146,7 +170,8 @@ public class PreferencesUtilsTest {
// then
assertEquals(layout.getFields().size(), 4);
assertEquals(layout.getProfile(), "walking");
assertEquals(layout.getName(), "walking");
assertEquals(layout.getColumnsPerRow(), 2);
assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
assertTrue(layout.getFields().get(0).isVisible());
@@ -177,20 +202,22 @@ public class PreferencesUtilsTest {
layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, false, false));
// when
PreferencesUtils.setCustomLayout(layoutSrc);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.stats_custom_layouts_key), layoutSrc.toCsv());
editor.commit();
// then
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layout_fields_key), null);
String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layouts_key), null);
assertNotNull(csv);
assertEquals(csv,
"road cycling;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;");
"road cycling;" + PreferencesUtils.getLayoutColumnsByDefault() + ";"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1,0;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0,0;");
Layout layoutDst = PreferencesUtils.getCustomLayout();
assertEquals(layoutSrc.getProfile(), layoutDst.getProfile());
assertEquals(layoutSrc.getName(), layoutDst.getName());
assertEquals(layoutSrc.getFields().size(), layoutDst.getFields().size());
for (int i = 0; i < layoutSrc.getFields().size(); i++) {
assertEquals(layoutSrc.getFields().get(i).getKey(), layoutDst.getFields().get(i).getKey());
@@ -198,4 +225,122 @@ public class PreferencesUtilsTest {
assertEquals(layoutSrc.getFields().get(i).isPrimary(), layoutDst.getFields().get(i).isPrimary());
}
}
@Test
public void testEditCustomLayouts() {
// update all custom layouts
// given a custom layout with two profiles
String cyclingProfile = "cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;";
String runningProfile = "running;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;";
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile);
editor.apply();
List<Layout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
// when cyling profile is updated
String cyclingProfileUpdated = "cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
List<Layout> layoutsToBeUpdated = new ArrayList<>();
layoutsToBeUpdated.add(Layout.fromCsv(cyclingProfileUpdated, resources));
layoutsToBeUpdated.add(Layout.fromCsv(runningProfile, resources));
PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated);
// then only updated profile is modified in the custom layouts
List<Layout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
}
@Test
public void testEditCustomLayout() {
// Update only one custom layout
// given a custom layout with two profiles
String cyclingProfile = "cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;";
String runningProfile = "running;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;";
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile);
editor.apply();
List<Layout> layoutsBefore = PreferencesUtils.getAllCustomLayouts();
// when cyling profile is updated
String cyclingProfileUpdated = "cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;";
Layout layoutToBeUpdated = Layout.fromCsv(cyclingProfileUpdated, resources);
PreferencesUtils.updateCustomLayout(layoutToBeUpdated);
// then only updated profile is modified in the custom layouts
List<Layout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
assertEquals(layoutsBefore.size(), 2);
assertEquals(layoutsAfter.size(), 2);
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
}
@Test
public void testGetCustomLayout_whenSelectedOneNotExists() {
// given a custom layout with two profiles and not existing custom layout selected
String cyclingProfile = "cycling;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;";
String runningProfile = "running;2;"
+ context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;"
+ context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;"
+ context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;";
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile);
editor.putString(context.getString(R.string.stats_custom_layout_selected_layout_key), "Not Exists");
editor.apply();
// when it gets the custom layout
Layout layout = PreferencesUtils.getCustomLayout();
// then the first one was returned
assertEquals(layout.getName(), "cycling");
}
}
+2 -1
View File
@@ -324,7 +324,8 @@ limitations under the License.
<activity android:name=".io.file.exporter.ExportActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.SettingsCustomLayoutActivity" />
<activity android:name=".settings.SettingsCustomLayoutListActivity" />
<activity android:name=".settings.SettingsCustomLayoutEditActivity" />
<activity
android:name=".ShowOnMapProxyActivity$KML"
@@ -5,12 +5,17 @@ import android.view.View;
import androidx.lifecycle.ViewModelProvider;
import java.util.List;
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.databinding.AggregatedStatsBinding;
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
public class AggregatedStatisticsActivity extends AbstractActivity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private AggregatedStatsBinding viewBinding;
private AggregatedStatisticsAdapter adapter;
@@ -21,8 +26,10 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
viewModel.getAggregatedStats(trackIds).observe(this, aggregatedStatistics -> {
if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
viewBinding.aggregatedStatsList.setAdapter(adapter);
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.os.Handler;
@@ -97,7 +98,10 @@ public class ControllerFragment extends Fragment implements View.OnTouchListener
return;
}
getCallback().recordStart();
Callback callback = (Callback) getContext();
if (callback != null) {
callback.recordStart();
}
});
viewBinding.controllerStop.setOnTouchListener(this);
@@ -114,6 +118,12 @@ public class ControllerFragment extends Fragment implements View.OnTouchListener
trackRecordingServiceConnection.startConnection(getContext());
}
@Override
public void onPause() {
super.onPause();
handlerUpdateTotalTime.removeCallbacksAndMessages(null);
}
@Override
public void onStop() {
super.onStop();
@@ -175,7 +185,10 @@ public class ControllerFragment extends Fragment implements View.OnTouchListener
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
getCallback().recordPause();
Callback callback = (Callback) getContext();
if (callback != null) {
callback.recordPause();
}
transition.resetTransition();
@@ -198,8 +211,12 @@ public class ControllerFragment extends Fragment implements View.OnTouchListener
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
getCallback().recordStop();
ActivityUtils.vibrate(getContext(), 1000);
Context context = getContext();
Callback callback = (Callback) context;
if (callback != null) {
callback.recordStop();
ActivityUtils.vibrate(context, 1000);
}
transition.resetTransition();
};
@@ -222,10 +239,6 @@ public class ControllerFragment extends Fragment implements View.OnTouchListener
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private Callback getCallback() {
return (Callback) getActivity();
}
public interface Callback {
void recordStart();
@@ -49,6 +49,10 @@ import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
@@ -58,12 +62,12 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -248,6 +252,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
trackRecordingServiceConnection.unbind(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
trackRecordingServiceConnection = null;
}
@Override
protected View getRootView() {
viewBinding = TrackListBinding.inflate(getLayoutInflater());
@@ -463,6 +474,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
return true;
}
if (itemId == R.id.list_context_menu_aggregated_stats) {
Intent intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class)
.putParcelableArrayListExtra(AggregatedStatisticsActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
@@ -1,5 +1,6 @@
package de.dennisguse.opentracks;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -18,6 +19,8 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.List;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -28,10 +31,10 @@ 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.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
@@ -232,6 +235,13 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
trackDataHub.stop();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
trackRecordingServiceConnection = null;
}
@Override
protected View getRootView() {
viewBinding = TrackRecordingBinding.inflate(getLayoutInflater());
@@ -256,6 +266,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_insert_marker) {
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
@@ -264,8 +279,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
return true;
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
if (item.getItemId() == R.id.track_detail_menu_select_layout) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
List<String> layoutNames = PreferencesUtils.getAllCustomLayoutNames();
builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setDefaultLayout(layoutNames.get(which)));
builder.create().show();
return true;
}
@@ -18,13 +18,13 @@ import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Layout layout;
private final Context context;
private final SettingsCustomLayoutItemClickListener itemClickListener;
public SettingsCustomLayoutAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
public SettingsCustomLayoutEditAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
this.context = context;
this.itemClickListener = itemClickListener;
this.layout = layout;
@@ -34,12 +34,12 @@ public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter<RecyclerVi
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
return new SettingsCustomLayoutAdapter.ViewHolder(view);
return new SettingsCustomLayoutEditAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutAdapter.ViewHolder viewHolder = (SettingsCustomLayoutAdapter.ViewHolder) holder;
SettingsCustomLayoutEditAdapter.ViewHolder viewHolder = (SettingsCustomLayoutEditAdapter.ViewHolder) holder;
DataField field = layout.getFields().get(position);
viewHolder.itemView.setTag(field.getKey());
viewHolder.title.setText(field.getTitle());
@@ -0,0 +1,95 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.Optional;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class SettingsCustomLayoutListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Layout> layoutList;
private final Context context;
private SettingsCustomLayoutProfileClickListener itemClickListener;
public SettingsCustomLayoutListAdapter(Context context, SettingsCustomLayoutProfileClickListener itemClickListener) {
this.context = context;
this.itemClickListener = itemClickListener;
layoutList = PreferencesUtils.getAllCustomLayouts();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_layout_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingsCustomLayoutListAdapter.ViewHolder viewHolder = (SettingsCustomLayoutListAdapter.ViewHolder) holder;
Layout layout = layoutList.get(position);
viewHolder.itemView.setTag(layout.getName());
viewHolder.title.setText(layout.getName());
}
@Override
public int getItemCount() {
if (layoutList == null) {
return 0;
}
return layoutList.size();
}
public List<Layout> getLayouts() {
return layoutList;
}
public void reloadLayouts() {
layoutList = PreferencesUtils.getAllCustomLayouts();
notifyDataSetChanged();
}
public void removeLayout(int position) {
layoutList.remove(position);
PreferencesUtils.updateCustomLayouts(layoutList);
notifyDataSetChanged();
}
public void restoreItem(Layout layout, int position) {
layoutList.add(position, layout);
PreferencesUtils.updateCustomLayouts(layoutList);
notifyDataSetChanged();
}
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView title;
public ViewHolder(@NonNull View itemView) {
super(itemView);
title = itemView.findViewById(R.id.custom_layout_title);
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
String profile = (String) view.getTag();
Optional<Layout> optionalLayout = layoutList.stream().filter(layout -> layout.sameName(new Layout(profile))).findFirst();
optionalLayout.ifPresent(itemClickListener::onSettingsCustomLayoutProfileClicked);
}
}
public interface SettingsCustomLayoutProfileClickListener {
void onSettingsCustomLayoutProfileClicked(@NonNull Layout layout);
}
}
@@ -2,11 +2,13 @@ package de.dennisguse.opentracks.content.data;
import android.content.Context;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.CsvConstants;
import de.dennisguse.opentracks.util.CsvLayoutUtils;
public class DataField {
public class DataField implements Parcelable {
public static final String YES_VALUE = "1";
public static final String NOT_VALUE = "0";
@@ -24,6 +26,26 @@ public class DataField {
this.isWide = isWide;
}
protected DataField(Parcel in) {
key = in.readString();
title = in.readString();
isVisible = in.readByte() != 0;
isPrimary = in.readByte() != 0;
isWide = in.readByte() != 0;
}
public static final Creator<DataField> CREATOR = new Creator<DataField>() {
@Override
public DataField createFromParcel(Parcel in) {
return new DataField(in);
}
@Override
public DataField[] newArray(int size) {
return new DataField[size];
}
};
public String getKey() {
return key;
}
@@ -63,7 +85,8 @@ public class DataField {
public String toCsv() {
String visible = this.isVisible ? YES_VALUE : NOT_VALUE;
String primary = this.isPrimary ? YES_VALUE : NOT_VALUE;
return key + CsvConstants.ITEM_SEPARATOR + visible + CsvConstants.ITEM_SEPARATOR + primary;
String wide = this.isWide ? YES_VALUE : NOT_VALUE;
return key + CsvLayoutUtils.PROPERTY_SEPARATOR + visible + CsvLayoutUtils.PROPERTY_SEPARATOR + primary + CsvLayoutUtils.PROPERTY_SEPARATOR + wide;
}
public static String getTitleByKey(Resources resources, String key) {
@@ -109,4 +132,18 @@ public class DataField {
throw new RuntimeException("It doesn't exists a field with key: " + key);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(key);
parcel.writeString(title);
parcel.writeByte((byte) (isVisible ? 1 : 0));
parcel.writeByte((byte) (isPrimary ? 1 : 0));
parcel.writeByte((byte) (isWide ? 1 : 0));
}
}
@@ -1,16 +1,75 @@
package de.dennisguse.opentracks.content.data;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Layout {
private final String profile;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.CsvLayoutUtils;
public class Layout implements Parcelable {
private static final String TAG = Layout.class.getSimpleName();
// User-generated layout's name.
private final String name;
private int columnsPerRow;
private final List<DataField> dataFields = new ArrayList<>();
public Layout(String profile) {
this.profile = profile;
public static Layout fromCsv(@NonNull String csvLine, @NonNull Resources resources) {
List<String> csvParts = CsvLayoutUtils.getCsvLineParts(csvLine);
if (csvParts == null) {
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
return new Layout(PreferencesUtils.getDefaultLayoutName());
}
Layout layout = new Layout(csvParts.get(0), Integer.parseInt(csvParts.get(1)));
for (int i = 2; i < csvParts.size(); i++) {
String[] fieldParts = CsvLayoutUtils.getCsvFieldParts(csvParts.get(i));
if (fieldParts == null) {
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
return layout;
}
layout.addField(fieldParts[0], DataField.getTitleByKey(resources, fieldParts[0]), fieldParts[1].equals(DataField.YES_VALUE), fieldParts[2].equals(DataField.YES_VALUE), fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
}
return layout;
}
public Layout(String name) {
this.name = name;
this.columnsPerRow = PreferencesUtils.getLayoutColumnsByDefault();
}
public Layout(String name, int columnsPerRow) {
this.name = name;
this.columnsPerRow = columnsPerRow;
}
protected Layout(Parcel in) {
name = in.readString();
columnsPerRow = in.readInt();
in.readList(dataFields, DataField.class.getClassLoader());
}
public static final Creator<Layout> CREATOR = new Creator<Layout>() {
@Override
public Layout createFromParcel(Parcel in) {
return new Layout(in);
}
@Override
public Layout[] newArray(int size) {
return new Layout[size];
}
};
public void addField(String key, String title, boolean visible, boolean primary, boolean isWide) {
dataFields.add(new DataField(key, title, visible, primary, isWide));
}
@@ -27,6 +86,11 @@ public class Layout {
dataFields.remove(dataField);
}
public void replaceAllFields(List<DataField> newFields) {
dataFields.clear();
addFields(newFields);
}
public List<DataField> getFields() {
return new ArrayList<>(dataFields);
}
@@ -36,7 +100,46 @@ public class Layout {
dataFields.add(to, dataFieldToMove);
}
public String getProfile() {
return profile;
public String getName() {
return name;
}
public int getColumnsPerRow() {
return columnsPerRow;
}
public void setColumnsPerRow(int columnsPerRow) {
this.columnsPerRow = columnsPerRow;
}
public boolean sameName(Layout layout) {
return this.name.equalsIgnoreCase(layout.getName());
}
public boolean sameName(String name) {
return this.name.equalsIgnoreCase(name);
}
public String toCsv() {
List<DataField> fields = getFields();
if (fields.isEmpty()) {
return "";
}
return getName() + CsvLayoutUtils.ITEM_SEPARATOR + getColumnsPerRow() + CsvLayoutUtils.ITEM_SEPARATOR
+ fields.stream().map(DataField::toCsv).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR))
+ CsvLayoutUtils.ITEM_SEPARATOR;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeInt(columnsPerRow);
parcel.writeList(dataFields);
}
}
@@ -349,6 +349,19 @@ public class TrackPoint {
return this;
}
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
if (hasSensorDistance() && lastTrackPoint.hasSensorDistance()) {
sensorDistance = sensorDistance.minus(lastTrackPoint.getSensorDistance());
}
if (hasAltitudeGain() && lastTrackPoint.hasAltitudeGain()) {
altitudeGain_m -= lastTrackPoint.altitudeGain_m;
}
if (hasAltitudeLoss() && lastTrackPoint.hasAltitudeLoss()) {
altitudeLoss_m -= lastTrackPoint.altitudeLoss_m;
}
return this;
}
public boolean hasSensorData() {
return hasHeartRate() || hasCadence() || hasPower();
}
@@ -205,6 +205,26 @@ public class ContentProviderUtils {
return tracks;
}
public List<Track> getTracks(List<Track.Id> trackIds) {
if (trackIds == null || trackIds.isEmpty()) {
return getTracks();
}
String selection = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
String[] selectionArgs = trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new);
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(selection, selectionArgs, TracksColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
tracks.ensureCapacity(cursor.getCount());
do {
tracks.add(createTrack(cursor));
} while (cursor.moveToNext());
}
}
return tracks;
}
public Track getTrack(@NonNull Track.Id trackId) {
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())}, null)) {
if (cursor != null && cursor.moveToNext()) {
@@ -29,6 +29,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
/**
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
@@ -65,13 +65,10 @@ public class StatisticsRecordingFragment extends Fragment {
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layout_fields_key, key)) {
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
updateUInecessary = true;
layout = PreferencesUtils.getCustomLayout();
}
if (PreferencesUtils.isKey(R.string.stats_custom_layout_columns_key, key)) {
gridLayoutManager.setSpanCount(PreferencesUtils.getLayoutColumns());
gridLayoutManager.setSpanCount(layout.getColumnsPerRow());
}
if (key != null && updateUInecessary && isResumed()) {
@@ -107,13 +104,13 @@ public class StatisticsRecordingFragment extends Fragment {
RecyclerView recyclerView = viewBinding.statsRecyclerView;
recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL));
final int numColumns = PreferencesUtils.getLayoutColumns();
gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
layout = PreferencesUtils.getCustomLayout();
gridLayoutManager = new GridLayoutManager(getContext(), layout.getColumnsPerRow());
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (statisticsAdapter.isItemWide(position)) {
return numColumns;
return layout.getColumnsPerRow();
}
return 1;
}
@@ -6,6 +6,7 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
@@ -24,6 +25,7 @@ public class ExportService extends JobIntentService {
private static final String EXTRA_TRACK_ID = "extra_track_id";
private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
private static final String TAG = ExportService.class.getSimpleName();
public static void enqueue(Context context, ExportServiceResultReceiver receiver, Track.Id trackId, TrackFileFormat trackFileFormat, Uri directoryUri) {
Intent intent = new Intent(context, JobService.class);
@@ -42,18 +44,23 @@ public class ExportService extends JobIntentService {
TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT);
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
// Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
// Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
if (directoryFile == null || !directoryFile.canWrite()) {
Log.e(TAG, "Can't write to directory: " + directoryFile);
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
return;
}
// Export.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
// Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
// Send result to the receiver.
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
resultReceiver.send(resultCode, bundle);
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
@@ -24,7 +25,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
DocumentFile directoryUri = configureDirectoryChooserIntent(intent);
if (directoryUri == null) {
if (!isDirectoryValid(directoryUri)) {
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
} else {
startActivity(createNextActivityIntent(directoryUri.getUri())); //TODO Refactor to DocumentFile
@@ -32,6 +33,10 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
}
}
protected boolean isDirectoryValid(final DocumentFile directoryUri) {
return directoryUri != null && directoryUri.isDirectory() && directoryUri.canRead();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
@@ -83,6 +88,11 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
return PreferencesUtils.getDefaultExportDirectoryUri(this);
}
@Override
protected boolean isDirectoryValid(final DocumentFile directoryUri) {
return super.isDirectoryValid(directoryUri) && directoryUri.canWrite();
}
@Override
protected Intent createNextActivityIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
@@ -99,14 +109,15 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
switch (resultCode) {
case RESULT_OK:
Uri directoryUri = resultData.getData();
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), PreferencesUtils.getDefaultExportDirectoryUri(this));
Uri directoryUri = resultData.getData();
PreferencesUtils.setDefaultExportDirectoryUri(directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri);
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), directoryUri, resultData.getFlags());
break;
case RESULT_CANCELED:
IntentUtils.releaseDirectoryAccessPermission(getApplicationContext(), PreferencesUtils.getDefaultExportDirectoryUri(this));
PreferencesUtils.setDefaultExportDirectoryUri(null);
//TODO Remove stored permission contentResolver.releasePersistableUriPermission
break;
}
@@ -114,12 +125,19 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
finish();
}
@Override
protected boolean isDirectoryValid(final DocumentFile directoryUri) {
return super.isDirectoryValid(directoryUri) && directoryUri.canWrite();
}
@Override
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
if (PreferencesUtils.isDefaultExportDirectoryUri(this)) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(this).getUri());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(this).getUri());
}
}
return null;
}
@@ -17,9 +17,9 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
@@ -37,6 +37,7 @@ class TrackRecordingManager {
private TrackStatisticsUpdater trackStatisticsUpdater;
private boolean currentSegmentHasTrackPoint;
private TrackPoint lastTrackPoint;
private TrackPoint lastStoredTrackPoint;
TrackRecordingManager(Context context) {
@@ -80,11 +81,13 @@ class TrackRecordingManager {
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(trackId, segmentStartTrackPoint);
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null; }
void pause(TrackPointCreator trackPointCreator) {
insertTrackPoint(trackId, trackPointCreator.createSegmentEnd());
currentSegmentHasTrackPoint = false;
lastTrackPoint = null;
lastStoredTrackPoint = null;
}
@@ -94,6 +97,7 @@ class TrackRecordingManager {
trackId = null;
trackStatisticsUpdater = null;
lastTrackPoint = null;
lastStoredTrackPoint = null;
currentSegmentHasTrackPoint = false;
}
@@ -103,7 +107,7 @@ class TrackRecordingManager {
return null;
}
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastStoredTrackPoint);
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPoint);
tmpTrackStatisticsUpdater.addTrackPoint(current.first, recordingDistanceInterval);
@@ -161,13 +165,20 @@ class TrackRecordingManager {
return true;
}
if (trackPoint.hasSensorData() || (distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving())) {
if (distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval) && trackPoint.isMoving()) {
insertTrackPoint(trackId, trackPoint);
return true;
}
}
Log.d(TAG, "Not recording TrackPoint, idle");
if (lastStoredTrackPoint != null && trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
// Moving from non-moving to moving or vice versa; required to compute moving time correctly.
insertTrackPoint(trackId, trackPoint);
return true;
}
Log.d(TAG, "Not recording TrackPoint");
lastTrackPoint = trackPoint;
return false;
}
@@ -176,6 +187,22 @@ class TrackRecordingManager {
}
private void insertTrackPoint(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
if (lastTrackPoint != null) {
if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time.");
} else {
insertTrackPointHelper(trackId, lastTrackPoint);
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
trackPoint.minusCumulativeSensorData(lastTrackPoint);
}
lastTrackPoint = null;
}
insertTrackPointHelper(trackId, trackPoint);
}
private void insertTrackPointHelper(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
try {
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -69,6 +69,8 @@ public abstract class BluetoothConnectionManager<DataType> {
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
clearData();
break;
}
}
@@ -130,14 +132,18 @@ public abstract class BluetoothConnectionManager<DataType> {
observer.onChanged(sensorData);
}
private synchronized void clearData() {
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
}
synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
return;
}
bluetoothGatt.close();
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
clearData();
bluetoothGatt = null;
}
@@ -1,34 +1,73 @@
package de.dennisguse.opentracks.settings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.CsvLayoutUtils;
class PreferencesOpenHelper {
private static final int PREFERENCES_VERSION = 1;
private final int version;
private PreferencesOpenHelper() {
private PreferencesOpenHelper(int version) {
this.version = version;
}
static PreferencesOpenHelper newInstance() {
return new PreferencesOpenHelper();
static PreferencesOpenHelper newInstance(int version) {
return new PreferencesOpenHelper(version);
}
void checkForUpgrade() {
void check() {
int lastVersion = PreferencesUtils.getInt(R.string.prefs_last_version_key, 0);
if (PREFERENCES_VERSION > lastVersion) {
if (version > lastVersion) {
onUpgrade();
} else if (version < lastVersion) {
onDowngrade();
}
}
private void onUpgrade() {
PreferencesUtils.setInt(R.string.prefs_last_version_key, PREFERENCES_VERSION);
switch (PREFERENCES_VERSION) {
case 1:
upgradeFrom0to1();
PreferencesUtils.setInt(R.string.prefs_last_version_key, version);
for (int i = 1; i <= version; i++) {
switch (i) {
case 1:
upgradeFrom0to1();
break;
case 2:
upgradeFrom1to2();
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + version);
}
}
}
private void upgradeFrom0to1() {
PreferencesUtils.setString(R.string.stats_custom_layout_fields_key, PreferencesUtils.buildDefaultLayout());
String preferenceValue = PreferencesUtils.getString(R.string.stats_custom_layouts_key, "");
if (preferenceValue.isEmpty()) {
PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout());
}
}
private void upgradeFrom1to2() {
String csvVersion1CustomLayout = PreferencesUtils.getString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout());
ArrayList<String> parts = new ArrayList<>();
Collections.addAll(parts, csvVersion1CustomLayout.split(CsvLayoutUtils.ITEM_SEPARATOR));
if (parts.size() < 2) {
PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout());
return;
}
if (!parts.get(1).matches("\\d+")) {
parts.add(1, String.valueOf(PreferencesUtils.getLayoutColumnsByDefault()));
}
PreferencesUtils.setString(R.string.stats_custom_layouts_key, parts.stream().collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR)));
}
private void onDowngrade() {
PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout());
}
}
@@ -25,26 +25,25 @@ import android.content.res.TypedArray;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager;
import java.time.Duration;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.CsvConstants;
import de.dennisguse.opentracks.util.CsvLayoutUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -57,6 +56,8 @@ public class PreferencesUtils {
private final static String TAG = PreferencesUtils.class.getSimpleName();
private static final int PREFERENCES_VERSION = 2;
private PreferencesUtils() {
}
@@ -71,7 +72,7 @@ public class PreferencesUtils {
PreferencesUtils.resources = resources;
PreferencesUtils.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
PreferencesOpenHelper.newInstance().checkForUpgrade();
PreferencesOpenHelper.newInstance(PREFERENCES_VERSION).check();
}
public static void registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener changeListener) {
@@ -94,7 +95,7 @@ public class PreferencesUtils {
/**
* Gets a preference key
*
* @param keyId the key id
* @param keyId the key id
*/
private static String getKey(int keyId) {
return resources.getString(keyId);
@@ -112,7 +113,7 @@ public class PreferencesUtils {
}
public static boolean isKey(int[] keyIds, String key) {
for(int keyId : keyIds) {
for (int keyId : keyIds) {
if (isKey(keyId, key)) {
return true;
}
@@ -185,7 +186,7 @@ public class PreferencesUtils {
} else {
unit = resources.getString(R.string.stats_units_imperial);
}
setString( R.string.stats_units_key, unit);
setString(R.string.stats_units_key, unit);
}
public static boolean isReportSpeed(String category) {
@@ -546,76 +547,113 @@ public class PreferencesUtils {
return getDefaultExportDirectoryUri(context) != null;
}
public static int getLayoutColumns() {
return getInt(R.string.stats_custom_layout_columns_key, resources.getInteger(R.integer.stats_custom_layout_columns_default));
public static int getLayoutColumnsByDefault() {
return resources.getInteger(R.integer.stats_custom_layout_columns_default);
}
public static void setLayoutColumns(int columns) {
setInt(R.string.stats_custom_layout_columns_key, columns);
}
private static List<TypedArray> getMultiTypedArray(String key) {
List<TypedArray> typedArrays = new ArrayList<>();
try {
Class<R.array> resource = R.array.class;
Field field;
int i = 0;
do {
field = resource.getField(key + resources.getString(R.string.stats_custom_layout_fields_default_value_separator) + i);
typedArrays.add(resources.obtainTypedArray(field.getInt(null)));
i++;
} while (field != null); //TODO Catch no such field exception instead of using an endless loop.
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, e.getMessage());
}
return typedArrays;
private static List<TypedArray> getMultiTypedArray() {
return Stream.of(
R.array.stats_custom_layout_fields_default_value_0,
R.array.stats_custom_layout_fields_default_value_1,
R.array.stats_custom_layout_fields_default_value_2,
R.array.stats_custom_layout_fields_default_value_3,
R.array.stats_custom_layout_fields_default_value_4,
R.array.stats_custom_layout_fields_default_value_5,
R.array.stats_custom_layout_fields_default_value_6,
R.array.stats_custom_layout_fields_default_value_7,
R.array.stats_custom_layout_fields_default_value_8,
R.array.stats_custom_layout_fields_default_value_9,
R.array.stats_custom_layout_fields_default_value_10,
R.array.stats_custom_layout_fields_default_value_11,
R.array.stats_custom_layout_fields_default_value_12,
R.array.stats_custom_layout_fields_default_value_13,
R.array.stats_custom_layout_fields_default_value_14,
R.array.stats_custom_layout_fields_default_value_15,
R.array.stats_custom_layout_fields_default_value_16,
R.array.stats_custom_layout_fields_default_value_17,
R.array.stats_custom_layout_fields_default_value_18
).map(id -> resources.obtainTypedArray(id)).collect(Collectors.toList());
}
@SuppressLint("ResourceType")
private static String buildDefaultFields() {
List<TypedArray> fieldsArrays = getMultiTypedArray();
return fieldsArrays.stream().map(i -> i.getString(0) + CsvLayoutUtils.PROPERTY_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR))
+ CsvLayoutUtils.ITEM_SEPARATOR;
}
static String buildDefaultLayout() {
List<TypedArray> fieldsArrays = getMultiTypedArray("stats_custom_layout_fields_default_value");
return resources.getString(R.string.default_activity_default) + CsvConstants.LINE_SEPARATOR
+ fieldsArrays.stream().map(i -> i.getString(0) + CsvConstants.ITEM_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvConstants.LINE_SEPARATOR))
+ CsvConstants.LINE_SEPARATOR;
return resources.getString(R.string.stats_custom_layout_default_layout) + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields();
}
public static String getDefaultLayoutName() {
return resources.getString(R.string.stats_custom_layout_default_layout);
}
/**
* @return custom layout selected or the first one if any has been selected or the one selected is not exists anymore.
*/
public static Layout getCustomLayout() {
String csvCustomLayout = getString(R.string.stats_custom_layout_fields_key, buildDefaultLayout());
List<String> csvParts = Arrays.asList(csvCustomLayout.split(CsvConstants.LINE_SEPARATOR));
Layout layout = new Layout(csvParts.get(0));
for (int i = 1; i < csvParts.size(); i++) {
String[] fieldParts = csvParts.get(i).split(CsvConstants.ITEM_SEPARATOR);
layout.addField(fieldParts[0], DataField.getTitleByKey(resources, fieldParts[0]), fieldParts[1].equals(DataField.YES_VALUE), fieldParts[2].equals(DataField.YES_VALUE), fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayouts.split(CsvLayoutUtils.LINE_SEPARATOR);
String layoutSelected = getString(R.string.stats_custom_layout_selected_layout_key, null);
if (layoutSelected == null) {
return Layout.fromCsv(csvLines[0], resources);
}
return layout;
for (String line : csvLines) {
Layout layout = Layout.fromCsv(line, resources);
if (layout.sameName(layoutSelected)) {
return layout;
}
}
return Layout.fromCsv(csvLines[0], resources);
}
public static void setCustomLayout(Layout layout) {
List<DataField> fields = layout.getFields();
if (fields.isEmpty()) {
return;
public static void updateCustomLayouts(@NonNull List<Layout> layouts) {
setString(R.string.stats_custom_layouts_key, layouts.stream().map(Layout::toCsv).collect(Collectors.joining(CsvLayoutUtils.LINE_SEPARATOR)));
}
public static void updateCustomLayout(@NonNull Layout layout) {
List<Layout> preferenceLayouts = PreferencesUtils.getAllCustomLayouts();
Optional<Layout> layoutToBeUpdated = preferenceLayouts.stream().filter(l -> l.sameName(layout)).findFirst();
if (layoutToBeUpdated.isPresent()) {
layoutToBeUpdated.get().replaceAllFields(layout.getFields());
layoutToBeUpdated.get().setColumnsPerRow(layout.getColumnsPerRow());
PreferencesUtils.updateCustomLayouts(preferenceLayouts);
}
}
public static void addCustomLayout(@NonNull String layoutName) {
String newLayoutCsv = layoutName + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields();
String customLayoutCsv = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()) + CsvLayoutUtils.LINE_SEPARATOR + newLayoutCsv;
setString(R.string.stats_custom_layouts_key, customLayoutCsv);
}
public static void setDefaultLayout(String layoutName) {
setString(R.string.stats_custom_layout_selected_layout_key, layoutName);
}
public static List<Layout> getAllCustomLayouts() {
List<Layout> layouts = new ArrayList<>();
String csvCustomLayout = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
String[] csvLines = csvCustomLayout.split(CsvLayoutUtils.LINE_SEPARATOR);
for (String line : csvLines) {
layouts.add(Layout.fromCsv(line, resources));
}
String csv = layout.getProfile() + CsvConstants.LINE_SEPARATOR
+ fields.stream().map(DataField::toCsv).collect(Collectors.joining(CsvConstants.LINE_SEPARATOR))
+ CsvConstants.LINE_SEPARATOR;
setString(R.string.stats_custom_layout_fields_key, csv);
return layouts;
}
public static List<String> getAllCustomLayoutNames() {
return getAllCustomLayouts().stream().map(Layout::getName).collect(Collectors.toList());
}
public static void resetCustomLayoutPreferences() {
if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layout_fields_key))) {
if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layouts_key))) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(resources.getString(R.string.stats_custom_layout_fields_key));
editor.commit();
}
if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layout_columns_key))) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(resources.getString(R.string.stats_custom_layout_columns_key));
editor.remove(resources.getString(R.string.stats_custom_layouts_key));
editor.commit();
}
}
@@ -4,6 +4,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceFragmentCompat;
@@ -31,7 +32,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
if (savedInstanceState != null) {
fragment = (PreferenceFragmentCompat) getSupportFragmentManager().getFragment(savedInstanceState, FRAGMENT_KEY);
} else {
}
if (fragment == null) {
fragment = new MainSettingsFragment();
}
@@ -55,9 +57,11 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
getSupportFragmentManager().putFragment(outState, FRAGMENT_KEY, fragment);
if (fragment != null && fragment.isAdded()) {
getSupportFragmentManager().putFragment(outState, FRAGMENT_KEY, fragment);
}
}
@Override
@@ -18,18 +18,20 @@ import java.util.stream.IntStream;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.adapters.SettingsCustomLayoutAdapter;
import de.dennisguse.opentracks.adapters.SettingsCustomLayoutEditAdapter;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.content.data.DataField;
import de.dennisguse.opentracks.databinding.ActivitySettingsCustomLayoutBinding;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutActivity extends AbstractActivity implements SettingsCustomLayoutAdapter.SettingsCustomLayoutItemClickListener {
public class SettingsCustomLayoutEditActivity extends AbstractActivity implements SettingsCustomLayoutEditAdapter.SettingsCustomLayoutItemClickListener {
public static final String EXTRA_LAYOUT = "extraLayout";
private ActivitySettingsCustomLayoutBinding viewBinding;
private GridLayoutManager gridLayoutManager;
private SettingsCustomLayoutAdapter adapterFieldsVisible;
private SettingsCustomLayoutAdapter adapterFieldsHidden;
private SettingsCustomLayoutEditAdapter adapterFieldsVisible;
private SettingsCustomLayoutEditAdapter adapterFieldsHidden;
private String profile;
private Layout layoutFieldsVisible;
private Layout layoutFieldsHidden;
private int numColumns;
@@ -38,12 +40,13 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Recycler view with visible stats.
layoutFieldsVisible = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(), true);
adapterFieldsVisible = new SettingsCustomLayoutAdapter(this, this, layoutFieldsVisible);
Layout layout = getIntent().getParcelableExtra(EXTRA_LAYOUT);
profile = layout.getName();
layoutFieldsVisible = StatisticsUtils.filterVisible(layout, true);
adapterFieldsVisible = new SettingsCustomLayoutEditAdapter(this, this, layoutFieldsVisible);
numColumns = PreferencesUtils.getLayoutColumns();
numColumns = layout.getColumnsPerRow();
RecyclerView recyclerViewVisible = viewBinding.recyclerViewVisible;
gridLayoutManager = new GridLayoutManager(this, numColumns);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@@ -86,7 +89,6 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
numColumns = position + 1;
gridLayoutManager.setSpanCount(numColumns);
PreferencesUtils.setLayoutColumns(position + 1);
}
@Override
@@ -94,24 +96,26 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se
}
});
viewBinding.spinnerOptions.setSelection(PreferencesUtils.getLayoutColumns() - 1);
viewBinding.spinnerOptions.setSelection(numColumns - 1);
// Recycler view with not visible stats.
layoutFieldsHidden = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(), false);
adapterFieldsHidden = new SettingsCustomLayoutAdapter(this, this, layoutFieldsHidden);
layoutFieldsHidden = StatisticsUtils.filterVisible(layout, false);
adapterFieldsHidden = new SettingsCustomLayoutEditAdapter(this, this, layoutFieldsHidden);
RecyclerView recyclerViewNotVisible = viewBinding.recyclerViewNotVisible;
recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this));
recyclerViewNotVisible.setAdapter(adapterFieldsHidden);
setTitle(profile);
}
@Override
protected void onPause() {
super.onPause();
if (!layoutFieldsVisible.getFields().isEmpty() || !layoutFieldsHidden.getFields().isEmpty()) {
Layout newLayout = new Layout(layoutFieldsVisible.getProfile());
newLayout.addFields(layoutFieldsVisible.getFields());
newLayout.addFields(layoutFieldsHidden.getFields());
PreferencesUtils.setCustomLayout(newLayout);
Layout layout = new Layout(profile, numColumns);
layout.addFields(layoutFieldsVisible.getFields());
layout.addFields(layoutFieldsHidden.getFields());
PreferencesUtils.updateCustomLayout(layout);
}
}
@@ -0,0 +1,163 @@
package de.dennisguse.opentracks.settings;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.adapters.SettingsCustomLayoutListAdapter;
import de.dennisguse.opentracks.content.data.Layout;
import de.dennisguse.opentracks.databinding.ActivitySettingsCustomLayoutListBinding;
import de.dennisguse.opentracks.util.RecyclerViewSwipeDeleteCallback;
public class SettingsCustomLayoutListActivity extends AbstractActivity implements SettingsCustomLayoutListAdapter.SettingsCustomLayoutProfileClickListener {
private ActivitySettingsCustomLayoutListBinding viewBinding;
private SettingsCustomLayoutListAdapter adapter;
private RecyclerView recyclerView;
private View addProfileLayout;
private TextInputEditText addProfileEditText;
private TextInputLayout addProfileInputLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.custom_layout_list_title));
adapter = new SettingsCustomLayoutListAdapter(this, this);
recyclerView = viewBinding.recyclerView;
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
Button cancelButton = findViewById(R.id.custom_layout_list_cancel_button);
Button okButton = findViewById(R.id.custom_layout_list_ok_button);
cancelButton.setOnClickListener(view -> clearAndHideEditLayout());
okButton.setEnabled(false);
okButton.setOnClickListener(view -> {
PreferencesUtils.addCustomLayout(addProfileEditText.getText().toString());
clearAndHideEditLayout();
adapter.reloadLayouts();
});
addProfileLayout = findViewById(R.id.custom_layout_list_add_linear_layout);
addProfileInputLayout = findViewById(R.id.custom_layout_list_input_layout);
addProfileEditText = findViewById(R.id.custom_layout_list_edit_name);
addProfileEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s == null || s.toString().isEmpty()) {
okButton.setEnabled(false);
return;
}
if (adapter.getLayouts().stream().anyMatch(layout -> layout.sameName(s.toString()))) {
okButton.setEnabled(false);
addProfileInputLayout.setError(getString(R.string.custom_layout_list_edit_already_exists));
} else {
okButton.setEnabled(true);
addProfileInputLayout.setError("");
}
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
RecyclerViewSwipeDeleteCallback recyclerViewSwipeDeleteCallback = new RecyclerViewSwipeDeleteCallback(this) {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// When there's only one profile it cannot be deleted (so, "disable" movements: drag flags and swipe flags).
return adapter.getItemCount() > 1 ? makeMovementFlags(0, ItemTouchHelper.LEFT) : makeMovementFlags(0, 0);
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
final int position = viewHolder.getAdapterPosition();
final Layout item = adapter.getLayouts().get(position);
adapter.removeLayout(position);
Snackbar snackbar = Snackbar.make(recyclerView, getString(R.string.custom_layout_list_layout_removed), Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.generic_undo).toUpperCase(), view -> {
adapter.restoreItem(item, position);
recyclerView.scrollToPosition(position);
});
snackbar.show();
}
};
ItemTouchHelper itemTouchhelper = new ItemTouchHelper(recyclerViewSwipeDeleteCallback);
itemTouchhelper.attachToRecyclerView(recyclerView);
}
@Override
protected void onResume() {
super.onResume();
adapter.reloadLayouts();
}
@Override
protected View getRootView() {
PreferencesUtils.getCustomLayout();
viewBinding = ActivitySettingsCustomLayoutListBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.custom_layout_edit, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.custom_layout_edit_add_profile) {
addProfileLayout.setVisibility(View.VISIBLE);
addProfileEditText.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(addProfileEditText, InputMethodManager.SHOW_IMPLICIT);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onSettingsCustomLayoutProfileClicked(@NonNull Layout layout) {
Intent intent = new Intent(this, SettingsCustomLayoutEditActivity.class);
intent.putExtra(SettingsCustomLayoutEditActivity.EXTRA_LAYOUT, layout);
startActivity(intent);
}
private void clearAndHideEditLayout() {
addProfileEditText.setText("");
addProfileInputLayout.setError("");
addProfileLayout.setVisibility(View.GONE);
}
}
@@ -24,7 +24,7 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat {
Preference customLayoutPreference = findPreference(getString(R.string.stats_custom_layout_key));
customLayoutPreference.setOnPreferenceClickListener((preference) -> {
Intent intent = new Intent(getContext(), SettingsCustomLayoutActivity.class);
Intent intent = new Intent(getContext(), SettingsCustomLayoutListActivity.class);
startActivity(intent);
return true;
});
@@ -68,8 +68,6 @@ public class TrackStatisticsUpdater {
private final TrackStatistics currentSegment;
// Current segment's last trackPoint
private TrackPoint lastTrackPoint;
// Current segment's last moving trackPoint
private TrackPoint lastMovingTrackPoint;
public TrackStatisticsUpdater() {
this(new TrackStatistics());
@@ -96,7 +94,6 @@ public class TrackStatisticsUpdater {
this.speedBuffer_mps = new DoubleRingBuffer(toCopy.speedBuffer_mps);
this.lastTrackPoint = toCopy.lastTrackPoint;
this.lastMovingTrackPoint = toCopy.lastMovingTrackPoint;
}
public TrackStatistics getTrackStatistics() {
@@ -116,9 +113,8 @@ public class TrackStatisticsUpdater {
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
if (trackPoint.getType() == TrackPoint.Type.SEGMENT_START_MANUAL) {
if (trackPoint.isSegmentStart()) {
reset(trackPoint);
return;
}
if (!currentSegment.isInitialized()) {
@@ -129,7 +125,7 @@ public class TrackStatisticsUpdater {
currentSegment.setStopTime(trackPoint.getTime());
currentSegment.setTotalTime(Duration.between(currentSegment.getStartTime(), trackPoint.getTime()));
// Process sensor data
// Process sensor data: barometer
if (trackPoint.hasAltitudeGain()) {
currentSegment.addTotalAltitudeGain(trackPoint.getAltitudeGain());
}
@@ -138,62 +134,46 @@ public class TrackStatisticsUpdater {
currentSegment.addTotalAltitudeLoss(trackPoint.getAltitudeLoss());
}
if (trackPoint.hasSensorDistance()) {
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
}
// Update total distance
if (trackPoint.hasSensorDistance()) {
// Sensor-based distance/speed
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
} else if (lastTrackPoint != null && trackPoint.isMoving()) {
// GPS-based distance/speed
// Assumption: we ignore TrackPoints that are not moving as those are likely imprecise GPS measurements
Distance movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
if (movingDistance != null) {
currentSegment.addTotalDistance(movingDistance);
}
}
// Update moving time
if (trackPoint.isMoving() && lastTrackPoint != null && lastTrackPoint.isMoving()) {
Duration movingTime = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingTime.isNegative()) {
throw new RuntimeException("Moving time cannot be negative");
}
currentSegment.addMovingTime(movingTime);
// Update max speed
updateSpeed(trackPoint, lastTrackPoint);
} else {
speedBuffer_mps.reset();
}
if (trackPoint.isSegmentEnd()) {
reset(trackPoint);
return;
}
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
return;
}
if (!trackPoint.hasSensorDistance()
&& trackPoint.hasLocation() && lastMovingTrackPoint.hasLocation()) {
// GPS-based distance/speed
Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
if (movingDistance != null && movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset();
lastTrackPoint = trackPoint;
return; //TOOD Why? Is there nothing to be done afterwards?
}
// Update total distance
currentSegment.addTotalDistance(movingDistance);
}
Duration movingTime = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingTime.isNegative()) {
lastTrackPoint = trackPoint;
return;
}
// Update moving time
if (lastTrackPoint.isMoving()) {
currentSegment.addMovingTime(movingTime);
}
// Update max speed
if (trackPoint.hasSpeed() && lastTrackPoint.hasSpeed()) {
updateSpeed(trackPoint, lastTrackPoint);
}
if (trackPoint.getType() == TrackPoint.Type.SEGMENT_START_AUTOMATIC) {
reset(trackPoint);
return;
}
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
}
private void reset(TrackPoint trackPoint) {
@@ -203,7 +183,6 @@ public class TrackStatisticsUpdater {
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
lastMovingTrackPoint = null;
altitudeBuffer_m.reset();
speedBuffer_mps.reset();
}
@@ -1,5 +1,7 @@
package de.dennisguse.opentracks.util;
import static android.content.Context.VIBRATOR_SERVICE;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
@@ -14,6 +16,7 @@ import android.view.MenuItem;
import android.widget.AbsListView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import java.util.ArrayList;
@@ -21,8 +24,6 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.ContextualActionModeCallback;
import de.dennisguse.opentracks.R;
import static android.content.Context.VIBRATOR_SERVICE;
public class ActivityUtils {
private static final String TAG = ActivityUtils.class.getSimpleName();
@@ -75,11 +76,7 @@ public class ActivityUtils {
arrayList.add(key);
}
}
int[] result = new int[arrayList.size()];
for (int i = 0; i < arrayList.size(); i++) {
result[i] = arrayList.get(i);
}
return result;
return arrayList.stream().mapToInt(i -> i).toArray();
}
});
}
@@ -97,7 +94,7 @@ public class ActivityUtils {
return searchView;
}
public static void vibrate(Context context, int milliseconds) {
public static void vibrate(@NonNull Context context, int milliseconds) {
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
@@ -107,9 +107,6 @@ public class BluetoothUtils {
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
}
/**
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
*/
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
@@ -118,26 +115,26 @@ public class BluetoothUtils {
}
int flags = characteristic.getValue()[0];
boolean hasCrank = (flags & 0x01) > 0;
boolean hasWheel = (flags & 0x02) > 0;
boolean hasWheel = (flags & 0x01) > 0;
boolean hasCrank = (flags & 0x02) > 0;
SensorDataCycling.Cadence cadence = null;
int index = 1;
if (hasCrank && valueLength - index >= 6) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
SensorDataCycling.DistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 6) {
int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
index += 4;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
index += 2;
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
}
SensorDataCycling.DistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 4) {
int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
SensorDataCycling.Cadence cadence = null;
if (hasCrank && valueLength - index >= 4) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelCount, wheelTime);
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
}
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
@@ -1,10 +0,0 @@
package de.dennisguse.opentracks.util;
public final class CsvConstants {
private CsvConstants() {
}
public static final String LINE_SEPARATOR = ";";
public static final String ITEM_SEPARATOR = ",";
}
@@ -0,0 +1,77 @@
package de.dennisguse.opentracks.util;
import androidx.annotation.Nullable;
import com.google.android.material.navigation.NavigationBarView;
import java.util.Arrays;
import java.util.List;
public final class CsvLayoutUtils {
public static final String LINE_SEPARATOR = "\n";
public static final String ITEM_SEPARATOR = ";";
public static final String PROPERTY_SEPARATOR = ",";
private CsvLayoutUtils() {
}
/**
* @param csvLine Layout description in a CSV format.
* @return All CSV parts from the csvLine or null if it's malformed.
*/
@Nullable
public static List<String> getCsvLineParts(String csvLine) {
if (csvLine == null) {
return null;
}
// The line must have three items, at least: layout's name, number of columns and one field.
List<String> csvParts = Arrays.asList(csvLine.split(ITEM_SEPARATOR));
if (csvParts.size() < 3 || !hasValue(csvParts.get(0)) || !isInt(csvParts.get(1))) {
return null;
}
return csvParts;
}
/**
* @param csvField Layout's field in a CSV format.
* @return List of field parts or null if it's malformed.
*/
@Nullable
public static String[] getCsvFieldParts(@Nullable String csvField) {
if (csvField == null) {
return null;
}
// Field must have three items: key, is visible (0 or 1), is primary (0 or 1).
String[] fieldParts = csvField.split(CsvLayoutUtils.PROPERTY_SEPARATOR);
if (fieldParts.length < 3 || !hasValue(fieldParts[0]) || !hasZeroOneValue(fieldParts[1]) || !hasZeroOneValue(fieldParts[2])) {
return null;
}
return fieldParts;
}
private static boolean hasValue(String value) {
return value != null && !value.isEmpty();
}
private static boolean isInt(String value) {
if (value == null || value.isEmpty()) {
return false;
}
try {
Integer.parseInt(value);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
private static boolean hasZeroOneValue(String value) {
return isInt(value) && (Integer.parseInt(value) == 0 || Integer.parseInt(value) == 1);
}
}
@@ -100,7 +100,7 @@ public class ExportUtils {
}
private static String getExportFileNameForTrack(Track track, String trackFileFormatExtension) {
return track.getUuid().toString().substring(0, 8) + "_" + track.getName() + "." + trackFileFormatExtension;
return track.getUuid().toString().substring(0, 8) + "_" + FileUtils.sanitizeFileName(track.getName()) + "." + trackFileFormatExtension;
}
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
@@ -149,7 +149,6 @@ public class FileUtils {
*
* @param name name
*/
// TODO Check if this function is still needed.
public static String sanitizeFileName(String name) {
StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.Intent;
import android.content.UriPermission;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
@@ -25,12 +26,14 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.DescriptionGenerator;
@@ -92,8 +95,9 @@ public class IntentUtils {
continue;
}
Pair<Uri, String> uriTrackFile = ShareContentProvider.createURI(trackId, track.getName(), PreferencesUtils.getExportTrackFileFormat());
Pair<Uri, String> uriSharePicture = ShareContentProvider.createURI(trackId, track.getName(), TrackFileFormat.SHARE_PICTURE_PNG);
String trackName = FileUtils.sanitizeFileName(track.getName());
Pair<Uri, String> uriTrackFile = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat());
Pair<Uri, String> uriSharePicture = ShareContentProvider.createURI(trackId, trackName, TrackFileFormat.SHARE_PICTURE_PNG);
uris.addAll(Arrays.asList(uriSharePicture.first, uriTrackFile.first));
}
@@ -203,8 +207,23 @@ public class IntentUtils {
return new Pair<>(intent, photoUri);
}
public static void persistDirectoryAccessPermission(Context context, Uri directoryUri) {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().takePersistableUriPermission(directoryUri, flags);
public static void persistDirectoryAccessPermission(Context context, Uri directoryUri, int existingFlags) {
int newFlags = existingFlags & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(directoryUri, newFlags);
}
public static void releaseDirectoryAccessPermission(Context context, final DocumentFile documentFile) {
if (documentFile == null) {
return;
}
final Uri documentUri = documentFile.getUri();
final List<UriPermission> persistedUriPermissions = context.getContentResolver().getPersistedUriPermissions();
for (final UriPermission permission : persistedUriPermissions) {
final Uri uri = permission.getUri();
if (uri.equals(documentUri)) {
context.getContentResolver().releasePersistableUriPermission(uri, 0);
}
}
}
}
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import de.dennisguse.opentracks.R;
abstract public class RecyclerViewSwipeDeleteCallback extends ItemTouchHelper.Callback {
private final Paint clearPaint;
private final ColorDrawable background;
private final int backgroundColor;
private final Drawable deleteDrawable;
private final int intrinsicWidth;
private final int intrinsicHeight;
public RecyclerViewSwipeDeleteCallback(Context context) {
background = new ColorDrawable();
backgroundColor = context.getResources().getColor(R.color.colorBackgroundDelete);
clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
deleteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_forever_24dp);
deleteDrawable.setTint(context.getResources().getColor(R.color.colorForegroundDelete));
intrinsicWidth = deleteDrawable.getIntrinsicWidth();
intrinsicHeight = deleteDrawable.getIntrinsicHeight();
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(0, ItemTouchHelper.LEFT);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) {
return false;
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
View itemView = viewHolder.itemView;
int itemHeight = itemView.getHeight();
boolean isCancelled = dX == 0 && !isCurrentlyActive;
if (isCancelled) {
clearCanvas(c, itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
return;
}
background.setColor(backgroundColor);
background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
background.draw(c);
int deleteIconTop = itemView.getTop() + (itemHeight - intrinsicHeight) / 2;
int deleteIconMargin = (itemHeight - intrinsicHeight) / 2;
int deleteIconLeft = itemView.getRight() - deleteIconMargin - intrinsicWidth;
int deleteIconRight = itemView.getRight() - deleteIconMargin;
int deleteIconBottom = deleteIconTop + intrinsicHeight;
deleteDrawable.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom);
deleteDrawable.draw(c);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
private void clearCanvas(Canvas c, Float left, Float top, Float right, Float bottom) {
c.drawRect(left, top, right, bottom, clearPaint);
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return 0.7f;
}
}
@@ -25,7 +25,7 @@ public class StatisticsUtils {
}
public static Layout filterVisible(Layout layout, boolean visible) {
Layout result = new Layout(layout.getProfile());
Layout result = new Layout(layout.getName());
result.addFields(layout.getFields().stream().filter(f -> f.isVisible() == visible).collect(Collectors.toList()));
return result;
}
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.viewmodels;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -20,18 +21,18 @@ public class AggregatedStatisticsModel extends AndroidViewModel {
super(application);
}
public LiveData<AggregatedStatistics> getAggregatedStats() {
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable List<Track.Id> trackIds) {
if (aggregatedStats == null) {
aggregatedStats = new MutableLiveData<>();
loadAggregatedStats();
loadAggregatedStats(trackIds);
}
return aggregatedStats;
}
private void loadAggregatedStats() {
private void loadAggregatedStats(@Nullable List<Track.Id> trackIds) {
new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext());
List<Track> tracks = contentProviderUtils.getTracks();
List<Track> tracks = contentProviderUtils.getTracks(trackIds);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<LinearLayout
android:id="@+id/custom_layout_list_add_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_marginBottom="50dp"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/custom_layout_list_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:errorEnabled="true"
android:layout_margin="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/custom_layout_list_edit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:imeOptions="actionDone"
android:hint="@string/custom_layout_list_edit_name" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/custom_layout_list_bottom_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/custom_layout_list_cancel_button"
style="@style/BottomButton"
android:text="@android:string/cancel" />
<Button
android:id="@+id/custom_layout_list_ok_button"
style="@style/BottomButton"
android:text="@android:string/ok" />
</LinearLayout>
<View style="@style/HorizontalLine" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/StatsCardView">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/custom_layout_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Profile/Custom Layout Title"
style="@style/LargeLabel"
android:layout_margin="8dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/custom_layout_edit_add_profile"
android:icon="@drawable/ic_baseline_add_24"
android:orderInCategory="1"
app:showAsAction="ifRoom"
android:title="@string/menu_add_custom_layout" />
</menu>
+5
View File
@@ -35,6 +35,11 @@ limitations under the License.
android:icon="@drawable/ic_delete_forever_24dp"
android:title="@string/menu_delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/list_context_menu_aggregated_stats"
android:icon="@drawable/ic_statistics_24dp"
android:title="@string/menu_aggregated_statistics"
app:showAsAction="ifRoom" />
<item
android:id="@+id/list_context_menu_select_all"
android:icon="@drawable/ic_select_all_24dp"
+9 -3
View File
@@ -15,6 +15,11 @@ limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_map_24dp"
app:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" />
<item
android:id="@+id/track_detail_insert_marker"
android:icon="@drawable/ic_marker_add_24dp"
@@ -22,10 +27,11 @@ limitations under the License.
app:showAsAction="ifRoom"
android:title="@string/menu_insert_marker" />
<item
android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_map_24dp"
android:id="@+id/track_detail_menu_select_layout"
android:icon="@drawable/ic_ui"
android:orderInCategory="2"
app:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" />
android:title="@string/menu_select_layout" />
<item
android:id="@+id/track_detail_markers"
android:icon="@drawable/ic_marker_show_24dp"
Binary file not shown.
+28 -1
View File
@@ -547,7 +547,34 @@ limitations under the License.
<string name="permission_bluetooth_failed_rejected">V nastavení systému je třeba ručně povolit aplikaci OpenTracks přístup k blízkým zařízením.</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mil/h (doporučeno)</string>
<string name="settings_recording_idle_speed_title">Prahová hodnota volnoběhu</string>
<string name="settings_recording_idle_speed_title">Rychlost pro autopausu</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (doporučeno)</string>
<string name="stats_clock">Hodiny</string>
<string name="settings_sensors_summary">Cyklistické snímače, obvod kola, snímače běhu</string>
<string name="settings_defaults_summary">Preferované jednotky, Výchozí aktivita, Výchozí název stopy</string>
<string name="settings_reset_summary">Obnovení výchozích nastavení</string>
<string name="settings_activity_defaults_category">Výchozí nastavení aktivity</string>
<string name="settings_units_category">Jednotky</string>
<string name="settings_import_export_summary">Import, export, automatický export</string>
<string name="settings_import_export_title">Dovoz/vývoz</string>
<string name="settings_announcements_summary">Čas, vzdálenost, rychlost hlasu</string>
<string name="settings_announcements_title">Hlasová oznámení</string>
<string name="settings_sensors_title">Bluetooth senzory</string>
<string name="settings_gps_summary">Časový interval, Interval vzdálenosti, Maximální vzdálenost, Přesnost</string>
<string name="settings_ui_summary">Vzhled, Rozložení záznamu, Téma</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_open_tracks_summary">Vše o projektu OpenTracks</string>
<string name="settings_defaults_title">Výchozí jednotky a aktivity</string>
<string name="settings_ui_title">Uživatelské rozhraní</string>
<string name="settings_gps_title">GPS</string>
<string name="generic_undo">Zrušit</string>
<string name="menu_select_layout">Vyberte rozvržení</string>
<string name="custom_layout_list_title">Rozložení</string>
<string name="custom_layout_select_layout">Vyberte rozvržení</string>
<string name="menu_add_custom_layout">Nové rozvržení</string>
<string name="custom_layout_list_edit_name">Zadejte název rozvržení, které chcete přidat</string>
<string name="custom_layout_list_edit_already_exists">Název rozvržení již existuje</string>
<string name="custom_layout_list_layout_removed">Rozložení bylo odstraněno</string>
<string name="stats_custom_layout_default_layout">Výchozí rozložení</string>
</resources>
+18 -5
View File
@@ -456,7 +456,7 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="settings_night_mode_option_day">Claro</string>
<string name="settings_night_mode_option_night">Oscuro</string>
<string name="settings_night_mode_title">Elegir un tema</string>
<string name="settings_sensor_wheel_circumference">Perímetro de la rueda (mm)</string>
<string name="settings_sensor_wheel_circumference">Circunferencia de la rueda (mm)</string>
<string name="stats_split_interval">Intervalo</string>
<string name="track_detail_intervals_tab">Intervalos</string>
<string name="aggregated_stats_empty_message">Graba tu primera pista para ver estadísticas agregadas</string>
@@ -506,9 +506,9 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocidad de voz</string>
<string name="track_delete_not_recording">Esta pista no se puede borrar, ya que está grabada.</string>
<string name="settings_sensor_running_speed_and_cadence">Correr: distancia, velocidad y cadencia</string>
<string name="settings_sensor_cycling_distance_speed">Ciclismo: distancia y velocidad</string>
<string name="settings_sensor_cycling_cadence">Ciclismo: cadencia</string>
<string name="settings_sensor_running_speed_and_cadence">Distancia, velocidad y cadencia</string>
<string name="settings_sensor_cycling_distance_speed">Distancia y velocidad</string>
<string name="settings_sensor_cycling_cadence">Cadencia</string>
<string name="settings_announcements">Avisos</string>
<string name="settings_announcements_totaltime_frequency">Intervalo de tiempo</string>
<string name="settings_announcements_distance_frequency">Intervalo de distancia</string>
@@ -522,7 +522,7 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="settings_layout_reset_confirm_message">Todas los ajustes de distribución se revertirán a los valores por defecto. Esto no eliminará ningún recorrido en el dispositivo.</string>
<string name="settings_layout_reset_confirm_title">¿Restablecer todas las disposiciones\?</string>
<string name="settings_layout_reset_done">Todos los ajustes de disposición se han revertido a los valores por defecto</string>
<string name="settings_all_reset">Restablecer todos los ajustes</string>
<string name="settings_all_reset">Reiniciar</string>
<string name="location_coordinate">%1$s°</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="field_remove_from_layout">Remover</string>
@@ -531,4 +531,17 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="stats_coordinates">Latitud/Longitud</string>
<string name="title_activity_settings_custom_layout">Personalizar la pantalla de grabación</string>
<string name="field_set_secondary">Secundario</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recomendado)</string>
<string name="settings_recording_idle_speed_title">Umbral de velocidad de ralentí</string>
<string name="stats_clock">Reloj</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (recomendado)</string>
<string name="settings_generic_sensor">Genérico</string>
<string name="settings_cycling_sensor">Ciclismo</string>
<string name="permission_bluetooth_failed">OpenTracks requiere un permiso para utilizar el Bluetooth.</string>
<string name="permission_bluetooth_failed_rejected">Es necesario conceder manualmente a OpenTracks el acceso a los Dispositivos Cercanos en la configuración del sistema.</string>
<string name="settings_running_sensor">Corriendo</string>
<string name="settings_import_section">Importar</string>
<string name="settings_export_section">Exportar</string>
</resources>
+65 -4
View File
@@ -469,7 +469,7 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
<string name="sensor_state_heart_rate_avg">Keskmine pulsisagedus</string>
<string name="sensor_state_cadence_max">Max kadentsus</string>
<string name="sensor_state_cadence_avg">Keskmine kadentsus</string>
<string name="settings_sensor_wheel_circumference">Ratta ümbermõõt (mm)</string>
<string name="settings_sensor_wheel_circumference">Velje ümbermõõt (mm)</string>
<string name="stats_loss">Kahjum</string>
<string name="track_detail_intervals_tab">Intervallid</string>
<string name="stats_split_interval">Jagatud intervall</string>
@@ -506,7 +506,68 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Hääle kiirus</string>
<string name="track_delete_not_recording">Seda pala ei saa kustutada, kuna see on praegu salvestatud.</string>
<string name="settings_sensor_cycling_cadence">Jalgrattasport: cadence</string>
<string name="settings_sensor_cycling_distance_speed">Jalgrattasport: kaugus ja kiirus</string>
<string name="settings_sensor_running_speed_and_cadence">Running: kaugus, kiirus ja kadents</string>
<string name="settings_sensor_cycling_cadence">Rütm</string>
<string name="settings_sensor_cycling_distance_speed">Kaugus ja kiirus</string>
<string name="settings_sensor_running_speed_and_cadence">Kaugus, kiirus ja kadents</string>
<string name="stats_clock">Kell</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (soovitatav)</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="settings_recording_idle_speed_title">Tühikäigu kiiruse lävi</string>
<string name="settings_announcements_distance_frequency">Vahemaa intervall</string>
<string name="description_speed_source_gps">GPS</string>
<string name="settings_cycling_sensor">Jalgrattasõit</string>
<string name="settings_import_section">Import</string>
<string name="permission_bluetooth_failed">OpenTracks nõuab luba kasutada Bluetooth.</string>
<string name="permission_bluetooth_failed_rejected">Peate käsitsi andma Opentrackidele juurdepääsu lähedalasuvatele seadmetele süsteemi seadetes.</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="location_coordinate">%1$s°</string>
<string name="field_set_primary">Esmane</string>
<string name="generic_choose_an_option">Valige suvand</string>
<string name="settings_layout_reset_done">Kõik paigutusseaded on muudetud vaikeväärtusteks</string>
<string name="settings_all_reset">Lähtestage</string>
<string name="settings_defaults_title">Vaikeühikud ja tegevus</string>
<string name="settings_defaults_summary">Eelistatud Üksused, Vaiketegevus, Raja Vaikenimi</string>
<string name="settings_ui_title">kasutajaliides</string>
<string name="settings_ui_summary">Välimus, Salvestamise Paigutus, Teema</string>
<string name="settings_gps_summary">Ajavahemik, vahemaa intervall, maksimaalne kaugus, täpsus</string>
<string name="settings_sensors_title">Bluetooth Andurid</string>
<string name="settings_announcements_title">Häälteated</string>
<string name="settings_announcements_summary">Aeg, Kaugus, Hääle Kiirus</string>
<string name="settings_units_category">Üksus</string>
<string name="settings_import_export_summary">Import, Eksport, Automaatne Eksport</string>
<string name="settings_import_export_title">Import/Eksport</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_open_tracks_summary">Kõik OpenTracks Projekti Kohta</string>
<string name="settings_open_tracks_title">Avatud Rajad</string>
<string name="settings_reset_summary">Lähtestage Seaded vaikeväärtustele</string>
<string name="settings_recording_customize_layout_title">Kohandage oma salvestuspaigutust</string>
<string name="settings_recording_customize_layout_select_columns">Andmeväljad rea kohta</string>
<string name="settings_export_section">Eksport</string>
<string name="settings_announcements_totaltime_frequency">Ajavahemik</string>
<string name="settings_layout_reset">Kohandatud Paigutuse Lähtestamine</string>
<string name="settings_layout_reset_confirm_title">Lähtestage oma paigutused\?</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (soovitatav)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="stats_sensors_power">Võim</string>
<string name="stats_sensors_cadence">Rütm</string>
<string name="stats_sensors_heart_rate">Südamerütm</string>
<string name="field_set_secondary">Teisene</string>
<string name="title_activity_settings_custom_layout">Salvestamise Paigutuse Kohandamine</string>
<string name="settings_sensors_summary">Rattaandurid, Ratta Ümbermõõt, Jooksuandurid</string>
<string name="settings_activity_defaults_category">Tegevuse Vaikeväärtused</string>
<string name="field_remove_from_layout">Eemaldama</string>
<string name="stats_coordinates">Laiuskraad/Pikkuskraad</string>
<string name="settings_announcements">Teade</string>
<string name="settings_layout_reset_confirm_message">Kõik paigutuse seaded tagastatakse vaikeväärtustele. See ei kustuta seadmes olevaid lugusid.</string>
<string name="settings_running_sensor">Töötama</string>
<string name="settings_generic_sensor">Geneeriline</string>
<string name="custom_layout_list_layout_removed">Paigutus eemaldati</string>
<string name="menu_add_custom_layout">Uus kujundus</string>
<string name="generic_undo">Tagasi</string>
<string name="menu_select_layout">Valige paigutus</string>
<string name="stats_custom_layout_default_layout">Vaikimisi paigutus</string>
<string name="custom_layout_select_layout">Valige paigutus</string>
<string name="custom_layout_list_edit_already_exists">Paigutuse nimi on juba olemas</string>
<string name="custom_layout_list_edit_name">Sisestage paigutuse nimi, et lisada</string>
<string name="custom_layout_list_title">Plaanid</string>
</resources>
+34 -6
View File
@@ -457,7 +457,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<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="activity_type_escooter">éscooter</string>
<string name="settings_sensor_wheel_circumference">Circonférence de roue (millimètre)</string>
<string name="settings_sensor_wheel_circumference">Circonférence de la roue (mm)</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">Traces non exportées</string>
@@ -506,9 +506,9 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="delete_service">Service de suppression</string>
<string name="menu_voice_rate">Vitesse vocale</string>
<string name="track_delete_not_recording">Cette piste ne peut pas être supprimée car elle est en cours d\'enregistrement.</string>
<string name="settings_sensor_cycling_distance_speed">Cyclisme : distance et vitesse</string>
<string name="settings_sensor_cycling_cadence">Vélo : cadence</string>
<string name="settings_sensor_running_speed_and_cadence">Course à pied : distance, vitesse et cadence</string>
<string name="settings_sensor_cycling_distance_speed">Distance et vitesse</string>
<string name="settings_sensor_cycling_cadence">Cadre</string>
<string name="settings_sensor_running_speed_and_cadence">Distance, vitesse et cadence</string>
<string name="settings_layout_reset">Réinitialiser la disposition personnalisée</string>
<string name="settings_layout_reset_confirm_message">Tous les paramètres de disposition reviendront à leur valeur par défaut. Ceci ne supprimera aucun parcours de l\'appareil.</string>
<string name="stats_coordinates">Latitude/Longitude</string>
@@ -517,7 +517,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="description_speed_source_gps">GPS</string>
<string name="generic_choose_an_option">Choisissez une option</string>
<string name="stats_sensors_heart_rate">Fréquence cardiaque</string>
<string name="stats_sensors_cadence">Cadence</string>
<string name="stats_sensors_cadence">Cadences</string>
<string name="stats_sensors_power">Énergie</string>
<string name="permission_bluetooth_failed_rejected">Vous devez accorder manuellement l\'accès aux appareils à proximité à OpenTracks dans les paramètres système.</string>
<string name="settings_recording_customize_layout_title">Personnalisez votre disposition d\'enregistrement</string>
@@ -526,11 +526,39 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="settings_announcements_distance_frequency">Intervalle de distance</string>
<string name="settings_layout_reset_confirm_title">Réinitialiser vos dispositions \?</string>
<string name="settings_layout_reset_done">Tous les paramètres pas défaut sont revenus à leur valeur par défaut</string>
<string name="settings_all_reset">Réinitialiser tous les paramètres</string>
<string name="settings_all_reset">Réinitialiser</string>
<string name="location_coordinate">%1$s°</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="title_activity_settings_custom_layout">Personnaliser la disposition d\'enregistrement</string>
<string name="permission_bluetooth_failed">OpenTracks a besoin de la permission d\'utiliser le Bluetooth.</string>
<string name="settings_recording_customize_layout_select_columns">Champs de données pas rangée</string>
<string name="field_set_primary">Principal</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (recommandé)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommandé)</string>
<string name="settings_recording_idle_speed_title">Seuil de vitesse au ralenti</string>
<string name="stats_clock">Horloge</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="settings_generic_sensor">Générique</string>
<string name="settings_cycling_sensor">Cyclisme</string>
<string name="settings_running_sensor">Courant</string>
<string name="settings_import_section">Importer</string>
<string name="settings_export_section">Exporter</string>
<string name="settings_reset_summary">Réinitialiser les paramètres aux valeurs par défaut</string>
<string name="settings_activity_defaults_category">Défauts de l\'activité</string>
<string name="settings_sensors_summary">Capteurs de cyclisme, Circonférence de roue, Capteurs de course à pied</string>
<string name="settings_ui_summary">Apparence, Mise en page de l\'enregistrement, Thème</string>
<string name="settings_units_category">Unités</string>
<string name="settings_import_export_summary">Importez, Exportez, Auto Export</string>
<string name="settings_import_export_title">Importez/Exportez</string>
<string name="settings_announcements_summary">Temps, distance, vitesse de la voix</string>
<string name="settings_announcements_title">Annonces vocales</string>
<string name="settings_sensors_title">Intervalle de temps, Intervalle de distance, Distance maximale, Précision</string>
<string name="settings_gps_summary">Intervalle de temps, Intervalle de distance, Distance maximale, Précision</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_ui_title">Interface utilisateur</string>
<string name="settings_defaults_summary">Unités préférées, Activité par défaut, Suivi du nom par défaut</string>
<string name="settings_defaults_title">Unités et activité par défaut</string>
<string name="settings_open_tracks_summary">A propos du projet OpenTracks</string>
<string name="settings_open_tracks_title">Open Tracks</string>
</resources>
+29 -2
View File
@@ -494,13 +494,13 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
<string name="export_progress_summary_overwrite_msg">Rutas sobrescritas</string>
<string name="export_progress_summary_skip_msg">Rutas omitidas</string>
<string name="export_progress_summary_errors_msg">Erros</string>
<string name="generic_open_track">Open Track</string>
<string name="generic_open_track">Aberto O Control</string>
<string name="import_prevent_reimport">Estableceches previr a reimportación da ruta</string>
<string name="import_progress_summary_ok_msg">Ficheiros importados</string>
<string name="import_progress_summary_exists_msg">Xa existe</string>
<string name="import_progress_summary_errors_msg">Erros</string>
<string name="sensor_state_cadence_avg">Cadencia media</string>
<string name="sensor_state_cadence_max">Cadencia máx.</string>
<string name="sensor_state_cadence_max">Max Cadencia</string>
<string name="sensor_state_heart_rate_avg">Pulso medio</string>
<string name="sensor_state_heart_rate_max">Pulso máximo</string>
<string name="sensor_state_power_avg">Potencia media</string>
@@ -549,4 +549,31 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
<string name="value_float_mile_hour">%1$.1f km/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f km/h (recomendado)</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (recomendado)</string>
<string name="stats_clock">Reloxo</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_defaults_summary">O Preferido Unidades, Por Defecto Actividade, Controlar O Nome Por Defecto</string>
<string name="settings_announcements_summary">A Distancia, Tempo, Voz Velocidade</string>
<string name="settings_open_tracks_summary">Todo Sobre OpenTracks Proxecto</string>
<string name="settings_defaults_title">Por defecto Unidades e Actividade</string>
<string name="settings_ui_title">Interface De Usuario</string>
<string name="settings_ui_summary">Aparencia, Gravación, Deseño, Tema</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_gps_summary">Intervalo de tempo, Distancia intervalo, Max Distancia, Precisión</string>
<string name="settings_sensors_title">Bluetooth Sensores</string>
<string name="settings_sensors_summary">Ciclismo Sensores, Roda Circunferencia, Execución De Sensores</string>
<string name="settings_announcements_title">Voz Anuncios</string>
<string name="settings_import_export_title">Importación/Exportación</string>
<string name="settings_import_export_summary">Importación, Exportación, Auto Exportación</string>
<string name="settings_units_category">Unidades</string>
<string name="settings_activity_defaults_category">Actividade de Inadimplência</string>
<string name="settings_reset_summary">Axustar a Configuración de Valores por Defecto</string>
<string name="menu_select_layout">Elixe disposición</string>
<string name="custom_layout_list_title">Disposicións</string>
<string name="custom_layout_list_edit_name">Escribe o nome da nova disposición</string>
<string name="custom_layout_list_edit_already_exists">Xa existe unha con ese nome</string>
<string name="custom_layout_list_layout_removed">Disposición eliminada</string>
<string name="generic_undo">Desfacer</string>
<string name="menu_add_custom_layout">Nova disposición</string>
<string name="custom_layout_select_layout">Elixe disposición</string>
<string name="stats_custom_layout_default_layout">Disposición por defecto</string>
</resources>
+104 -45
View File
@@ -23,15 +23,13 @@ limitations under the License.
<string name="about_libraries">Librerie</string>
<!-- Help -->
<string name="help_feature_title">OpenTracks ha le seguenti caratteristiche:</string>
<string name="help_feature_content">
* crea tracciamenti GPS\n
* utilizza un sensore di frequenza cardiaca (solo Bluetooth LE)\n
* comunicazioni vocali (per tempo o distanza)\n
* crea marcatori (incl. immagini)\n
* funziona completamente offline\n
* mostra i tracciamenti su una mappa (vedi sotto)
</string>
<string name="help_track_map_title">Come mostrare un tracciamento su una mappa?</string>
<string name="help_feature_content">* crea tracce GPS
\n * utilizza un sensore di frequenza cardiaca (solo Bluetooth LE)
\n * comunicazioni vocali (per tempo o distanza)
\n * crea marcatori (incl. immagini)
\n * funziona completamente offline
\n * mostra i tracce su una mappa (vedi sotto)</string>
<string name="help_track_map_title">Come visualizzare una traccia su una mappa\?</string>
<string name="help_track_map_content">
Utilizzare la piccola icona della mappa.\n
a)
@@ -51,7 +49,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="help_data_google_play_services_title">OpenTracks richiede/utilizza Google Play Services?</string>
<string name="help_recording_title">Come fa OpenTracks a registrare i dati?</string>
<string name="help_data_storage_title">In che modo OpenTracks memorizza i dati dei sensori?</string>
<string name="help_gps_troubleshooting_title">OpenTracks non registra i luoghi?</string>
<string name="help_gps_troubleshooting_title">OpenTracks non registra la posizione\?</string>
<string name="help_support_title">Come posso supportare OpenTracks?</string>
<string name="open_track_as_KML">Apri come KML</string>
<string name="open_track_as_GPX">Apri come GPX</string>
@@ -72,7 +70,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="activity_type_driving_bus">guidare autobus</string>
<string name="activity_type_driving_car">guidare auto</string>
<string name="activity_type_ferry">traghetto</string>
<string name="activity_type_frisbee">di frisbee</string>
<string name="activity_type_frisbee">frisbee</string>
<string name="activity_type_gliding">aliante</string>
<string name="activity_type_hang_gliding">deltaplano</string>
<string name="activity_type_helicopter">elicottero</string>
@@ -136,7 +134,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="description_max_altitude">Altitudine max: %1$d m (%2$d ft)</string>
<string name="description_max_speed">Velocità massima: %1$.2f km/h (%2$.1f mi/h)</string>
<string name="description_min_altitude">Altitudine min: %1$d m (%2$d ft)</string>
<string name="description_moving_time">Tempo di spostamento: %1$s</string>
<string name="description_moving_time">Tempo in movimento: %1$s</string>
<string name="description_pace_imperial">Ritmo (min/mi)</string>
<string name="description_pace_metric">Ritmo (min/km)</string>
<string name="description_recorded_time">Registrato: %1$s</string>
@@ -150,7 +148,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="description_total_distance">Distanza totale: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_time">Tempo totale: %1$s</string>
<!-- Export -->
<string name="export_progress_message">Esportazioni su %1$s…</string>
<string name="export_progress_message">Esportando su %1$s…</string>
<!-- External Storage -->
<!-- Files -->
<plurals name="files">
@@ -159,7 +157,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
</plurals>
<!-- Generic -->
<string name="generic_add">Aggiungi</string>
<string name="generic_delete_selected_confirm_title">Eliminare selezione?</string>
<string name="generic_delete_selected_confirm_title">Eliminare selezionati\?</string>
<string name="generic_description">Descrizione</string>
<string name="generic_description_line">Descrizione: %1$s</string>
<string name="generic_name">Nome</string>
@@ -184,7 +182,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<!-- Import -->
<string name="import_progress_message">Importazione da %1$s in corso…</string>
<!-- Marker -->
<string name="marker_add_error">Impossibile inserire un indicatore. Nessun segnale GPS.</string>
<string name="marker_add_error">Impossibile inserire un indicatore. Nessun segnale GPS. Riprova ancora.</string>
<string name="marker_add_success">È stato inserito un indicatore</string>
<string name="marker_add_canceled">Operazione annullata, nessun indicatore aggiunto</string>
<string name="marker_delete_one_confirm_message">L\'indicatore verrà eliminato definitivamente dal dispositivo</string>
@@ -203,7 +201,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="menu_delete">Elimina</string>
<string name="menu_edit">Modifica</string>
<string name="menu_export_all">Esporta tutto</string>
<string name="menu_help">Guida</string>
<string name="menu_help">Aiuto</string>
<string name="menu_import_all">Importa tutto</string>
<string name="menu_insert_marker">Inserisci indicatore</string>
<string name="menu_insert_photo">Inserisci foto</string>
@@ -248,21 +246,21 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="settings_recording_track_name_title">Nome percorso predefinito</string>
<!-- Settings Reset -->
<string name="settings_reset">Reimposta</string>
<string name="settings_reset_confirm_message">Tutte le impostazioni verranno ripristinate sui valori predefiniti.</string>
<string name="settings_reset_confirm_message">Tutte le impostazioni verranno ripristinate sui valori predefiniti. Questo non eliminerà alcuna traccia dal dispositivo.</string>
<string name="settings_reset_confirm_title">Reimpostare ora?</string>
<string name="settings_reset_done">Tutte le impostazioni sono ripristinate sui valori predefiniti</string>
<!-- Settings Sensor -->
<string name="settings_sensor_bluetooth">Bluetooth</string>
<!-- Settings Stats -->
<string name="settings_stats_rate_title">Velocità preferita</string>
<string name="settings_stats_rate_title">Unità di misura preferita</string>
<string name="settings_stats_units_imperial">Imperiale (mi, ft)</string>
<string name="settings_stats_units_metric">Metrico (km, m)</string>
<string name="settings_stats_units_title">Unità di misura preferite</string>
<!-- Share Track -->
<string name="share_track_share_file_body">Penso che potrebbe essere interessato a questa pista.
<string name="share_track_share_file_body">Penso che potresti essere interessato a questa traccia.
\n
\n%1$s</string>
<string name="share_track_subject">Desidero condividere un percorso con te</string>
<string name="share_track_subject">Desidero condividere una traccia con te</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Ritmo medio in movimento</string>
<string name="stats_average_moving_speed">Velocità media in movimento</string>
@@ -297,7 +295,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<!-- Track Widget -->
<!-- Tracks -->
<!-- Unit -->
<string name="unit_feet">pd</string>
<string name="unit_feet">ft</string>
<string name="unit_kilometer">km</string>
<string name="unit_kilometer_per_hour">km/h</string>
<string name="unit_meter">m</string>
@@ -306,14 +304,14 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min/mi</string>
<!-- Value -->
<string name="value_integer_feet">%1$d pd</string>
<string name="value_integer_feet_excellent_gps">%1$d pd (GPS forte)</string>
<string name="value_integer_feet_recommended">%1$d pd (consigliato)</string>
<string name="value_integer_feet">%1$d ft</string>
<string name="value_integer_feet_excellent_gps">%1$d ft (segnale GPS forte)</string>
<string name="value_integer_feet_recommended">%1$d ft (consigliato)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/h</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (GPS forte)</string>
<string name="value_integer_meter_poor_gps">%1$d m (GPS debole)</string>
<string name="value_integer_meter_excellent_gps">%1$d m (segnale GPS forte)</string>
<string name="value_integer_meter_poor_gps">%1$d m (segnale GPS debole)</string>
<string name="value_integer_meter_recommended">%1$d m (consigliato)</string>
<string name="value_integer_mile">%1$d mi</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
@@ -401,12 +399,12 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="waypoint_type_theater">teatro</string>
<string name="waypoint_type_toilet">toilette</string>
<string name="waypoint_type_tourist_information">informazioni turistiche</string>
<string name="waypoint_type_tree">.</string>
<string name="waypoint_type_tree">albero</string>
<string name="waypoint_type_tunnel">galleria</string>
<string name="waypoint_type_waterfall">cascata</string>
<string name="waypoint_type_water_fountain">fontana ad acqua</string>
<string name="waypoint_type_waypoint">tappa</string>
<string name="settings_night_mode_title">Tema dell\' interfaccia grafica</string>
<string name="settings_night_mode_title">Tema dell\'interfaccia utente</string>
<string name="settings_error_initial_values_restored">Mi dispiace, ma si è verificato un errore. Tutte le impostazioni sono state riportate ai valori predefiniti.</string>
<string name="stats_split_interval">Intervallo di divisione</string>
<string name="aggregated_stats_empty_message">Registra il tuo primo percorso per vedere le statistiche aggregate</string>
@@ -440,13 +438,13 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<item quantity="other">Completato con %1$d errori</item>
</plurals>
<string name="generic_click_twice_cancel">Premi il tasto indietro di nuovo per annullare</string>
<string name="gps_fixed_and_ready">GPS pronto e corretto</string>
<string name="gps_fixed_and_ready">GPS agganciato e pronto</string>
<string name="menu_insert_gallery_img">Inserisci un immagine dalla galleria</string>
<string name="permission_gps_failed">OpenTracks richiede il permesso di utilizzare il GPS.</string>
<string name="settings_default_trackfileformat">Formato file nell\' esportazione/condivisione</string>
<string name="settings_default_trackfileformat">Formato file di esportazione/condivisione</string>
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Durante la registrazione del percorso, mostra le statistiche senza sbloccare il dispositivo.</string>
<string name="description_default_speed_or_pace">Per tipo di attività</string>
<string name="export_all_do_it_for_all">Esegui ciò per tutti i conflitti</string>
<string name="export_all_do_it_for_all">applica lo stesso per tutti i conflitti</string>
<string name="export_track_errors">Percorsi non esportati</string>
<string name="generic_overwrite">Sovrascrivere</string>
<string name="generic_skip">Ignora</string>
@@ -457,7 +455,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="import_unsupported_format">Formato di file non supportato</string>
<string name="import_parser_error">Errore nel riconoscimento degli argomenti: %1$s</string>
<string name="menu_resume_track">Riprendi il percorso</string>
<string name="track_recording_notification_accuracy">Incertezza GPS: %1$s</string>
<string name="track_recording_notification_accuracy">Precisione GPS: %1$s</string>
<string name="import_unable_to_import_file">Non riesco ad importare il file: %1$s</string>
<string name="import_error_info">%1$s: %2$s</string>
<string name="import_error_list_dialog_title">File non importati</string>
@@ -479,29 +477,29 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="track_detail_intervals_tab">Intervalli</string>
<string name="interval_list_empty_message">Non c\'è ancora nessun intervallo</string>
<string name="voice_pace_lap">Tempo sul giro di %1$s</string>
<string name="share_image_body">Penso che potresti essere interessat@ in questa immagine.</string>
<string name="share_image_body">Penso che potresti essere interessat* a questa immagine.</string>
<string name="hold_to_stop">Tieni premuto per fermare</string>
<string name="hold_to_pause">Tieni premuto per mettere in pausa</string>
<string name="instant_export_enabled_summary">È stata completata l\'esportazione del percorso finito nella memoria interna del tuo dispositivo</string>
<string name="instant_export_enabled_summary">Esporta la traccia nella memoria al termine della registrazione</string>
<string name="instant_export_enabled_title">Esportazione istantanea post-allenamento</string>
<string name="generic_open_track">Traccia aperta</string>
<string name="import_prevent_reimport">È stato impostato impedisci reimportazione traccia</string>
<string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la directory di esportazione.</string>
<string name="import_prevent_reimport">Hai impostato di fare in modo da non reimportare la traccia</string>
<string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la cartella di esportazione.</string>
<string name="settings_prevent_reimport_tracks_title">Impedire la reimportazione delle tracce</string>
<string name="export_progress_summary_errors_msg">Errore</string>
<string name="export_progress_summary_errors_msg">Errori</string>
<string name="export_progress_summary_skip_msg">Tracce saltate</string>
<string name="export_progress_summary_overwrite_msg">Tracce sovrascritte</string>
<string name="export_progress_summary_new_msg">Nuove tracce esportate</string>
<string name="import_no_kml_file_found">Nessun dottore.file kml trovato in KMZ</string>
<string name="import_no_kml_file_found">Nessun file doc.kml trovato in KMZ</string>
<string name="import_progress_summary_ok_msg">File importati</string>
<string name="import_progress_summary_exists_msg">Esiste già</string>
<string name="import_progress_summary_errors_msg">Errore</string>
<string name="import_progress_summary_errors_msg">Errori</string>
<string name="sensor_state_power_avg">Potenza media</string>
<string name="sensor_state_heart_rate_max">Frequenza cardiaca max</string>
<string name="stats_loss">Discesa</string>
<string name="settings_sensor_wheel_circumference">Circonferenza della ruota (mm)</string>
<string name="settings_sensor_wheel_circumference">Circonferenza della Ruota (mm)</string>
<string name="description_altitude_loss">Perdita di quota: %1$d m (%2$d ft)</string>
<string name="sensor_state_heart_rate_avg">Tasso cardiaco medio</string>
<string name="sensor_state_heart_rate_avg">Battito cardiaco medio</string>
<string name="sensor_state_cadence_max">Cadenza max</string>
<string name="sensor_state_cadence_avg">Cadenza media</string>
<string name="track_distance_notification">Distanza: %1$s</string>
@@ -511,8 +509,69 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="delete_service">Servizio di eliminazione</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocità della voce</string>
<string name="track_delete_not_recording">Questa traccia non può essere cancellata perché è attualmente registrata.</string>
<string name="settings_sensor_cycling_cadence">Ciclismo: cadenza</string>
<string name="settings_sensor_cycling_distance_speed">Ciclismo: distanza e velocità</string>
<string name="settings_sensor_running_speed_and_cadence">Come mostrare una traccia su una mappa</string>
<string name="track_delete_not_recording">Questa traccia non può essere cancellata perché è attualmente in registrazione.</string>
<string name="settings_sensor_cycling_cadence">Cadenza</string>
<string name="settings_sensor_cycling_distance_speed">Distanza e Velocità</string>
<string name="settings_sensor_running_speed_and_cadence">Distanza, Velocità e Cadenza</string>
<string name="stats_clock">Orologio</string>
<string name="settings_announcements_totaltime_frequency">Intervallo di tempo</string>
<string name="settings_recording_idle_speed_title">Soglia velocità di movimento</string>
<string name="settings_announcements_distance_frequency">Intervallo di distanza</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="location_coordinate">%1$s°</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (consigliato)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (consigliato)</string>
<string name="settings_cycling_sensor">Ciclismo</string>
<string name="settings_running_sensor">Corsa</string>
<string name="settings_import_section">Importa</string>
<string name="settings_export_section">Esporta</string>
<string name="stats_coordinates">Latitudine/Longitudine</string>
<string name="field_remove_from_layout">Rimuovi</string>
<string name="field_set_primary">Primario</string>
<string name="field_set_secondary">Secondario</string>
<string name="description_speed_source_gps">GPS</string>
<string name="permission_bluetooth_failed_rejected">Devi concedere manualmente il permesso ad OpenTracks di accedere ai Dispositivi nelle Vicinanze nelle impostazioni di sistema.</string>
<string name="permission_bluetooth_failed">OpenTracks richiede il permesso di utilizzare il Bluetooth.</string>
<string name="stats_sensors_cadence">Cadenza</string>
<string name="generic_choose_an_option">Scegli un\'opzione</string>
<string name="settings_recording_customize_layout_title">Personalizza il layout di registrazione</string>
<string name="settings_recording_customize_layout_select_columns">Campi dati per riga</string>
<string name="settings_defaults_title">Unità di misura e Attività Predefinite</string>
<string name="settings_defaults_summary">Unità di misura preferite, Attività Predefinite, Nome Traccia Predefinito</string>
<string name="settings_sensors_summary">Sensori di Cicliaml, Circonferenza Ruota, Sensori Corsa</string>
<string name="settings_layout_reset_done">Tutte le impostazioni del layout sono state reimpostate ai valori predefiniti</string>
<string name="settings_generic_sensor">Generico</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="title_activity_settings_custom_layout">Personalizza Layout di Registrazione</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_open_tracks_summary">Tutto a proposito del progetto OpenTracks</string>
<string name="settings_ui_title">Interfaccia Utente</string>
<string name="settings_ui_summary">Aspetto, Layout durante la registrazione, Tema</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_gps_summary">Intervallo di tempo, Intervallo di distanza, Massima Distanza, Precisione</string>
<string name="settings_sensors_title">Sensori Bluetooth</string>
<string name="settings_announcements_title">Annunci Vocali</string>
<string name="settings_announcements_summary">Tempo, Distanza, Velocità della Voce</string>
<string name="settings_import_export_title">Importa/Esporta</string>
<string name="settings_import_export_summary">Importa, Esporta, Auto Esporta</string>
<string name="settings_units_category">Unità</string>
<string name="settings_activity_defaults_category">Predefiniti dell\'Attività</string>
<string name="settings_reset_summary">Reimposta Impostazioni ai Valori Predefiniti</string>
<string name="settings_announcements">Annunci</string>
<string name="settings_layout_reset">Reimposta Layout Personalizzato</string>
<string name="settings_layout_reset_confirm_message">Tutte le impostazioni di layout saranno reimpostate ai valori predefiniti. Questo non eliminerà alcuna traccia dal dispositivo.</string>
<string name="settings_layout_reset_confirm_title">Rempostare i tuoi layout\?</string>
<string name="settings_all_reset">Reimposta</string>
<string name="stats_sensors_heart_rate">Frequenza Cardiaca</string>
<string name="stats_sensors_power">Potenza</string>
<string name="custom_layout_list_title">Disegni</string>
<string name="custom_layout_list_edit_name">Digitare il nome del layout da aggiungere</string>
<string name="generic_undo">Annulla</string>
<string name="menu_select_layout">Seleziona il layout</string>
<string name="menu_add_custom_layout">Nuovo layout</string>
<string name="custom_layout_list_edit_already_exists">Il nome del layout già esiste</string>
<string name="custom_layout_list_layout_removed">Il layout è stato rimosso</string>
<string name="custom_layout_select_layout">Seleziona il layout</string>
<string name="stats_custom_layout_default_layout">Layout predefinito</string>
</resources>
+31 -4
View File
@@ -101,7 +101,7 @@ limitations under the License.
<string name="description_speed_ms">Hastighet (m/s)</string>
<string name="description_sensor_cadence">Kadens (o/min)</string>
<string name="description_sensor_heart_rate">Puls (slag/min)</string>
<string name="description_sensor_power">Effekt (W)</string>
<string name="description_sensor_power">Kraft (W)</string>
<string name="description_speed_imperial">Hastighet (mi/hr)</string>
<string name="description_speed_metric">Hastighet (km/t)</string>
<string name="description_time">Tid</string>
@@ -182,7 +182,7 @@ limitations under the License.
<string name="sensor_state_cadence_value">%1$d o/min</string>
<string name="sensor_state_heart_rate">Puls</string>
<string name="sensor_state_heart_rate_value">%1$d slag/min</string>
<string name="sensor_state_power">Effekt</string>
<string name="sensor_state_power">Kraft</string>
<string name="sensor_state_power_value">%1$d W</string>
<string name="settings_not_while_recording">Ikke tilgjengelig mens du tar opp et spor</string>
<string name="settings_show">Vis</string>
@@ -490,7 +490,7 @@ limitations under the License.
<string name="sensor_state_cadence_max">Maks kadens</string>
<string name="sensor_state_heart_rate_avg">Gjennomsnittlig puls</string>
<string name="sensor_state_heart_rate_max">Maks puls</string>
<string name="sensor_state_power_avg">Gjennomsnittlig effekt</string>
<string name="sensor_state_power_avg">Gjennomsnittlig kraft</string>
<string name="settings_sensor_wheel_circumference">Hjulomkrets (mm)</string>
<string name="track_distance_notification">Distanse: %1$s</string>
<string name="track_speed_notification">Hastighet: %1$s</string>
@@ -515,7 +515,7 @@ limitations under the License.
<string name="settings_layout_reset_done">Alle layoutinnstillinger er tilbakestilt til standardverdiene</string>
<string name="settings_all_reset">Tilbakestill</string>
<string name="stats_sensors_cadence">Kadens</string>
<string name="stats_sensors_power">Effekt</string>
<string name="stats_sensors_power">Kraft</string>
<string name="location_coordinate">%1$s°</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="title_activity_settings_custom_layout">Tilpass opptakslayouten</string>
@@ -537,4 +537,31 @@ limitations under the License.
<string name="settings_recording_idle_speed_title">Terskel for tomgangshastighet</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (anbefales)</string>
<string name="stats_clock">Klokke</string>
<string name="settings_open_tracks_title">OpenTracks</string>
<string name="settings_defaults_summary">Foretrukne enheter, Standardaktivitet, Standard-spornavn</string>
<string name="settings_sensors_summary">Sykkelsensorer, Hjulomkrets, Løpesensorer</string>
<string name="settings_open_tracks_summary">Alt om prosjektet OpenTracks</string>
<string name="settings_defaults_title">Standardenheter og aktivitet</string>
<string name="settings_ui_title">Brukerflate</string>
<string name="settings_ui_summary">Utseende, Opptakslayout, Tema</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_gps_summary">Tidsintervall, Distanseintervall, Maks distanse, Nøyaktighet</string>
<string name="settings_sensors_title">Bluetooth-sensorer</string>
<string name="settings_announcements_title">Talemeldinger</string>
<string name="settings_announcements_summary">Tid, Distanse, Talehastighet</string>
<string name="settings_import_export_title">Import/Eksport</string>
<string name="settings_import_export_summary">Import, Eksport, Auto-eksport</string>
<string name="settings_units_category">Enheter</string>
<string name="settings_activity_defaults_category">Aktiviteters standardvalg</string>
<string name="settings_reset_summary">Tilbakestill innstillinger til standardverdier</string>
<string name="generic_undo">Angre</string>
<string name="menu_select_layout">Velg layout</string>
<string name="menu_add_custom_layout">Ny layout</string>
<string name="custom_layout_list_title">Layouter</string>
<string name="custom_layout_list_edit_name">Skriv inn navn på layout for å legge til</string>
<string name="custom_layout_list_edit_already_exists">Layoutens navn finnes allerede</string>
<string name="custom_layout_list_layout_removed">Layout ble fjernet</string>
<string name="custom_layout_select_layout">Velg layout</string>
<string name="stats_custom_layout_default_layout">Standard layout</string>
</resources>
+66 -5
View File
@@ -239,7 +239,7 @@ limitations under the License.
<string name="settings_sensor_bluetooth">Bluetooth</string>
<string name="settings_sensor_connecting">Podłączony do %1$s</string>
<string name="settings_sensor_disconnected">Rozłaczony z %1$s</string>
<string name="settings_sensor_wheel_circumference">Obwód koła (mm)</string>
<string name="settings_sensor_wheel_circumference">Obwód Koła (mm)</string>
<!-- Settings Stats -->
<string name="settings_stats_rate_title">Wybór tempo/prędkość</string>
<string name="settings_stats_units_imperial">Imperialne (mile, stopy)</string>
@@ -481,7 +481,7 @@ limitations under the License.
<string name="generic_separator_done_total">/</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Ukończono z jednym błędem</item>
<item quantity="few">Ukończone z czyimiś błędami</item>
<item quantity="few">Ukończone z %1$d błędami</item>
<item quantity="many">Zakończono z błędami %1$d</item>
<item quantity="other">Zakończono z błędami %1$d</item>
</plurals>
@@ -522,7 +522,68 @@ limitations under the License.
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Szybkość głosu</string>
<string name="track_delete_not_recording">Tej ścieżki nie można usunąć, ponieważ jest ona aktualnie nagrywana.</string>
<string name="settings_sensor_cycling_distance_speed">Kolarstwo: dystans i prędkość</string>
<string name="settings_sensor_cycling_cadence">Jazda na rowerze: kadencja</string>
<string name="settings_sensor_running_speed_and_cadence">Bieganie: dystans, prędkość i kadencja</string>
<string name="settings_sensor_cycling_distance_speed">Dystans i Prędkość</string>
<string name="settings_sensor_cycling_cadence">Tempo</string>
<string name="settings_sensor_running_speed_and_cadence">Dystans, Prędkość i Tempo</string>
<string name="settings_ui_title">Interfejs Użytkownika</string>
<string name="settings_gps_summary">Przedział czasu, Przedział dystansu, Maksymalny dystans, Dokładność</string>
<string name="settings_recording_customize_layout_title">Zmień swój układ ekranu podczas śledzenia</string>
<string name="settings_announcements">Wiadomości</string>
<string name="settings_announcements_totaltime_frequency">Przedział czasu</string>
<string name="stats_sensors_heart_rate">Częstość akcji serca</string>
<string name="stats_clock">Zegar</string>
<string name="settings_announcements_distance_frequency">Przedział dystansu</string>
<string name="settings_recording_idle_speed_title">Próg prędkości bezobciążeń</string>
<string name="settings_layout_reset_confirm_title">Zresetować twoje układy\?</string>
<string name="settings_cycling_sensor">Kolarstwo</string>
<string name="settings_running_sensor">Bieganie</string>
<string name="settings_import_section">Import</string>
<string name="settings_export_section">Eksport</string>
<string name="settings_generic_sensor">Domyślny</string>
<string name="description_speed_source_gps">GPS</string>
<string name="permission_bluetooth_failed">OpenTracks wymaga uprawnień do urzycia Bluetooth.</string>
<string name="generic_undo">Cofnij</string>
<string name="generic_choose_an_option">Wybierz opcje</string>
<string name="menu_select_layout">Wybierz układ</string>
<string name="menu_add_custom_layout">Nowy układ</string>
<string name="permission_bluetooth_failed_rejected">Należy manualnie zezwolić OpenTracks na dostęp do Najbliższych Urządzeń w ustawieniach.</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="settings_ui_summary">Wygląd, Układ ekranu śledzenia, Motyw</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_sensors_title">Czujnik Bluetooth</string>
<string name="settings_sensors_summary">Czujniki rowerowe, Obwód koła, Czujniki biegowe</string>
<string name="settings_announcements_title">Wiadomości głosowe</string>
<string name="settings_announcements_summary">Czas, Dystans, Głos prędkości</string>
<string name="settings_import_export_title">Import/Eksport</string>
<string name="settings_units_category">Jednostki</string>
<string name="settings_activity_defaults_category">Domyślne działania</string>
<string name="settings_reset_summary">Przywróć domyślne ustawienia</string>
<string name="settings_import_export_summary">Import, Eksport, Automatyczny eksport</string>
<string name="settings_recording_customize_layout_select_columns">Ilość pól na wiersz</string>
<string name="custom_layout_select_layout">Wybierz układ</string>
<string name="custom_layout_list_title">Układy</string>
<string name="custom_layout_list_edit_name">Napisz nazwę układu, aby dodać</string>
<string name="custom_layout_list_edit_already_exists">Układ o takiej nazwie już istnieje</string>
<string name="custom_layout_list_layout_removed">Układ został usunięty</string>
<string name="stats_custom_layout_default_layout">Domyślny układ</string>
<string name="settings_layout_reset_confirm_message">Wszystkie ustawienia zostaną zresetowane do wartości domyślnych.</string>
<string name="settings_layout_reset_done">Wszystkie ustawienia wróciły do wartości domyślnych</string>
<string name="settings_all_reset">Zresetuj</string>
<string name="settings_layout_reset">Zresetuj Układ</string>
<string name="stats_coordinates">Szerokość/długość geograficzna</string>
<string name="stats_sensors_cadence">Tempo</string>
<string name="stats_sensors_power">Moc</string>
<string name="location_coordinate">%1$s°</string>
<string name="location_latitude_longitude">%1$s, %2$s</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (zalecane)</string>
<string name="value_float_mile_hour">%1$.1f mi/h</string>
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (zalecane)</string>
<string name="title_activity_settings_custom_layout">Zmień układ ekranu podczas śledzenia</string>
<string name="field_remove_from_layout">Usuń</string>
<string name="field_set_primary">Główny</string>
<string name="field_set_secondary">Poboczny</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_open_tracks_summary">Wszystko o projekcie OpenTacks</string>
<string name="settings_defaults_title">Domyślne Jednostki i Aktywności</string>
<string name="settings_defaults_summary">Preferowane Jednostki, Domyślne Aktywności, Nazwy Treningów</string>
</resources>
+27
View File
@@ -488,4 +488,31 @@
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (Recomendado)</string>
<string name="settings_recording_idle_speed_title">Limite de velocidade de marcha lenta</string>
<string name="stats_clock">Relógio</string>
<string name="settings_announcements_title">Anúncios de voz</string>
<string name="settings_sensors_summary">Voice announcements</string>
<string name="settings_sensors_title">Sensores Bluetooth</string>
<string name="settings_gps_summary">Intervalo de tempo, Intervalo de distância, Distância máxima, Precisão</string>
<string name="settings_ui_summary">Aparência, Layout de gravação, Tema</string>
<string name="settings_ui_title">Interface do usuário</string>
<string name="settings_defaults_title">Unidades e Atividade Default</string>
<string name="settings_defaults_summary">Unidades Preferidas, Atividade Padrão, Nome Padrão da Pista</string>
<string name="settings_open_tracks_title">Trilhas abertas</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_reset_summary">Redefinir configurações para valores padrão</string>
<string name="settings_activity_defaults_category">Defaults da atividade</string>
<string name="settings_units_category">Unidades</string>
<string name="settings_import_export_summary">Importar, Exportar, Auto Export</string>
<string name="settings_import_export_title">Importar/Exportar</string>
<string name="settings_announcements_summary">Tempo, distância, velocidade da voz</string>
<string name="settings_open_tracks_summary">Tudo sobre o Projeto OpenTracks</string>
<string name="stats_custom_layout_default_layout">Layout padrão</string>
<string name="custom_layout_list_edit_already_exists">O nome do layout já existe</string>
<string name="custom_layout_list_edit_name">Digite o nome do layout a ser adicionado</string>
<string name="menu_add_custom_layout">Novo Layout</string>
<string name="generic_undo">Desfazer</string>
<string name="menu_select_layout">Selecione o layout</string>
<string name="custom_layout_list_title">Estrutura</string>
<string name="custom_layout_select_layout">Selecionar layout</string>
<string name="custom_layout_list_layout_removed">Layout foi removido</string>
</resources>
+27
View File
@@ -528,4 +528,31 @@ limitations under the License.
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (khuyến khích)</string>
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
<string name="value_float_kilometer_hour_recommended">%1$.1f km/h (khuyến khích)</string>
<string name="stats_clock">Cái đồng hồ</string>
<string name="settings_open_tracks_title">Mở Bài Hát</string>
<string name="settings_reset_summary">Thiết lập lại các thiết Lập để mặc Định giá Trị</string>
<string name="settings_activity_defaults_category">Hoạt động của Mặc định</string>
<string name="settings_units_category">Đơn vị</string>
<string name="settings_gps_summary">Khoảng thời gian, khoảng Cách, Max khoảng Cách, chính Xác</string>
<string name="settings_import_export_summary">Nhập Khẩu, Xuất Khẩu, Xuất Khẩu Tự Động</string>
<string name="settings_open_tracks_summary">Tất Cả Về Dự Án OpenTracks</string>
<string name="settings_defaults_title">Định Vị và các Hoạt động</string>
<string name="settings_defaults_summary">Các Đơn Vị Ưa Thích Định Hoạt Động Theo Dõi Tên Mặc Định</string>
<string name="settings_ui_title">Dùng Diện</string>
<string name="settings_ui_summary">Xuất Hiện, Ghi Lại Bố Trí Chủ Đề</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_sensors_title">Cảm Biến Bluetooth</string>
<string name="settings_sensors_summary">Đi Xe Đạp Cảm Biến, Bánh Vòng, Chạy Bộ Cảm Biến</string>
<string name="settings_announcements_title">Giọng Nói Thông Báo</string>
<string name="settings_announcements_summary">Thời Gian, Khoảng Cách, Tốc Độ Giọng Nói</string>
<string name="settings_import_export_title">Xuất Nhập Khẩu</string>
<string name="generic_undo">Hoàn tác</string>
<string name="menu_select_layout">Chọn bố cục</string>
<string name="menu_add_custom_layout">Bố cục mới</string>
<string name="custom_layout_list_title">Bố cục</string>
<string name="custom_layout_list_edit_name">Nhập tên bố cục để thêm</string>
<string name="custom_layout_list_edit_already_exists">Tên bố cục đã tồn tại</string>
<string name="custom_layout_list_layout_removed">Bố cục đã bị xoá</string>
<string name="custom_layout_select_layout">Chọn bố cục</string>
<string name="stats_custom_layout_default_layout">Bố cục mặc định</string>
</resources>
+3
View File
@@ -30,6 +30,9 @@ limitations under the License.
<color name="colorAccent">@android:color/holo_orange_dark</color>
<color name="colorAccentTransparent">#55FF8800</color>
<color name="colorBackgroundDelete">#F44336</color>
<color name="colorForegroundDelete">#FEFEFE</color>
<color name="recording_text">#FFBE161B</color> <!-- red -->
<color name="stats_separator">#99999999</color> <!-- light gray -->
<color name="text_gradient_center">#B2000000</color> <!-- black 70% opacity -->
+3 -20
View File
@@ -12,40 +12,22 @@
<!-- Main settings -->
<string name="settings_open_tracks_key" translatable="false">settingsOpenTracks</string>
<string name="settings_open_tracks_title" translatable="false">Open Tracks</string>
<string name="settings_open_tracks_summary">All About OpenTracks Project</string>
<string name="settings_defaults_key" translatable="false">settingsDefaults</string>
<string name="settings_defaults_title">Default Units and Activity</string>
<string name="settings_defaults_summary">Preferred Units, Default Activity, Track Default Name</string>
<string name="settings_ui_key" translatable="false">settingsUI</string>
<string name="settings_ui_title">User Interface</string>
<string name="settings_ui_summary">Appearance, Recording Layout, Theme</string>
<string name="settings_gps_key" translatable="false">settingsGps</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_gps_summary">Time interval, Distance interval, Max Distance, Accuracy</string>
<string name="settings_sensors_key" translatable="false">settingsSensorsGps</string>
<string name="settings_sensors_title">Bluetooth Sensors</string>
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
<string name="settings_announcements_key" translatable="false">settingsAnnouncements</string>
<string name="settings_announcements_title">Voice Announcements</string>
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
<string name="settings_import_export_key" translatable="false">settingsImportExport</string>
<string name="settings_import_export_title">Import/Export</string>
<string name="settings_import_export_summary">Import, Export, Auto Export</string>
<!-- Setting's categories -->
<string name="settings_units_category">Units</string>
<string name="settings_activity_defaults_category">Activity\'s Defaults</string>
<string name="settings_reset_key" translatable="false">settingsReset</string>
<string name="settings_reset_summary">Reset Settings to Default Values</string>
<string name="settings_layout_reset_key" translatable="false">settingsLayoutReset</string>
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
@@ -64,6 +46,7 @@
<string name="settings_import" translatable="false">settingsImport</string>
<string name="settings_export" translatable="false">settingsExport</string>
<!-- Stats layout customization default layout -->
<string name="default_activity_key" translatable="false">defaultActivity</string>
<string name="default_activity_default" translatable="false">@string/activity_type_unknown</string>
<string-array name="activity_types">
@@ -328,11 +311,11 @@
<string name="stats_custom_layout_key" translatable="false">statsCustomLayoutKey</string>
<!-- Stats layout customization: number of columns per row -->
<string name="stats_custom_layout_columns_key" translatable="false">statsCustomLayoutColumnsKey</string>
<integer name="stats_custom_layout_columns_default" translatable="false">2</integer>
<!-- Stats layout customization: key -->
<string name="stats_custom_layout_fields_key" translatable="false">statsCustomLayoutFieldsKey</string>
<string name="stats_custom_layouts_key" translatable="false">statsCustomLayoutFieldsKey</string>
<string name="stats_custom_layout_selected_layout_key" translatable="false">statsCustomSelectedLayoutKey</string>
<!-- Stats keys -->
<string name="stats_custom_layout_distance_key" translatable="false">distance</string>
<string name="stats_custom_layout_total_time_key" translatable="false">total_time</string>
+38
View File
@@ -238,6 +238,7 @@ limitations under the License.
<string name="generic_completed">Completed</string>
<string name="generic_click_twice_cancel">Click back again to cancel</string>
<string name="generic_choose_an_option">Choose an option</string>
<string name="generic_undo">Undo</string>
<!-- Gps -->
<string name="gps_starting">Starting GPS</string>
<string name="gps_wait_for_better_signal">Waiting for a better GPS signal</string>
@@ -298,11 +299,13 @@ limitations under the License.
<string name="menu_select_all">Select all</string>
<string name="menu_settings">Settings</string>
<string name="menu_share">Share</string>
<string name="menu_select_layout">Select Layout</string>
<string name="menu_show_on_map">Show on map</string>
<string name="menu_start_gps">Start GPS</string>
<string name="menu_stop_gps">Stop GPS</string>
<string name="menu_voice_rate">Voice speed</string>
<string name="menu_resume_track">Resume track</string>
<string name="menu_add_custom_layout">New Layout</string>
<!-- OpenTracks -->
<string name="recording_service">Recording service</string>
<string name="delete_service">Deletion service</string>
@@ -328,6 +331,34 @@ limitations under the License.
<string name="bluetooth_disabled">Please enable Bluetooth.</string>
<string name="settings_not_while_recording">Not available while recording a track</string>
<string name="settings_show">Show</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="settings_open_tracks_summary">All About OpenTracks Project</string>
<string name="settings_defaults_title">Default Units and Activity</string>
<string name="settings_defaults_summary">Preferred Units, Default Activity, Track Default Name</string>
<string name="settings_ui_title">User Interface</string>
<string name="settings_ui_summary">Appearance, Recording Layout, Theme</string>
<string name="settings_gps_title">GPS</string>
<string name="settings_gps_summary">Time interval, Distance interval, Max Distance, Accuracy</string>
<string name="settings_sensors_title">Bluetooth Sensors</string>
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
<string name="settings_announcements_title">Voice Announcements</string>
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
<string name="settings_import_export_title">Import/Export</string>
<string name="settings_import_export_summary">Import, Export, Auto Export</string>
<string name="settings_units_category">Units</string>
<string name="settings_activity_defaults_category">Activity\'s Defaults</string>
<string name="settings_reset_summary">Reset Settings to Default Values</string>
<string name="settings_layout_reset_key" translatable="false">settingsLayoutReset</string>
<!-- Settings Advanced -->
<string name="settings_advanced">Advanced</string>
<string name="settings_default_trackfileformat">Export/sharing file format</string>
@@ -367,6 +398,13 @@ limitations under the License.
<string name="settings_announcements_distance_frequency">Distance interval</string>
<string name="settings_reset">Reset</string>
<!-- Custom Layout -->
<string name="custom_layout_list_title">Layouts</string>
<string name="custom_layout_list_edit_name">Type layout\'s name to add</string>
<string name="custom_layout_list_edit_already_exists">Layout\'s name already exists</string>
<string name="custom_layout_list_layout_removed">Layout was removed</string>
<string name="custom_layout_select_layout">Select Layout</string>
<string name="stats_custom_layout_default_layout">Default Layout</string>
<!-- Custom Layout Reset -->
<string name="settings_layout_reset">Reset Custom Layout</string>
<string name="settings_layout_reset_confirm_message">All layout settings will be reverted to the default values. This will not delete any tracks on the device.</string>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>