Merge upstream updates into local fork

This commit is contained in:
2026-09-11 14:10:42 +02:00
73 changed files with 1220 additions and 275 deletions
+5 -5
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:9.3.1' classpath 'com.android.tools.build:gradle:9.4.0'
} }
} }
@@ -47,8 +47,8 @@ android {
defaultConfig { defaultConfig {
applicationId 'de.dennisguse.opentracks' applicationId 'de.dennisguse.opentracks'
versionCode 6698 versionCode 6751
versionName "v4.28.0" versionName "v4.28.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -145,12 +145,12 @@ android {
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0' implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.1.0' implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation 'com.google.android.material:material:1.14.0' implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation 'androidx.core:core:1.19.0' implementation 'androidx.core:core:1.19.0'
implementation 'androidx.core:core-splashscreen:1.2.0' implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1' implementation 'androidx.mediarouter:mediarouter:1.8.1'
@@ -0,0 +1,10 @@
v4.28.1: OpenTracks
Changes:
- HealthConnect: sending total distance, speed and elevation gain
Bugfix:
- Gravel activity was not selectable
Developer:
- Cleanup ContentProvider URLs
@@ -0,0 +1,7 @@
v4.28.2: OpenTracks
Changes:
- Import: ignore GPX waypoints without time
Bugfix:
- Only start GPS once
@@ -0,0 +1,10 @@
v4.28.3: OpenTracks
Changes:
- Settings: allow to disable segment creation (max recording distance)
- Importer: match activity type case-insensitive
- GPX: export moving speed in summary
Bugfix:
- GPX: importer must respect namespace prefixes
@@ -1,15 +1,15 @@
v3.0.0: OpenTracks v3.0.0: OpenTracks
Primer lanzamiento de OpenTracks (es decir, una bifurcación de MyTracks) Primera versión de OpenTracks (es decir, una bifurcación dura de MyTracks)
Cambios: Cambios:
- Cambio de nombre de MyTracks a OpenTracks - Cambio de nombre de MyTracks a OpenTracks
- Se agregó un sensor de frecuencia cardíaca Bluetooth LE - Se ha añadido el sensor de frecuencia cardíaca Bluetooth LE
- Se eliminaron todas las funciones que requieren acceso a Internet. - Se han eliminado todas las funciones que requerían acceso a Internet
- Se eliminó la integración de Google Maps - Se ha eliminado la integración con Google Maps
- Se eliminó el uso de los servicios de Google Play. - Se ha eliminado el uso de los servicios de Google Play
- Se eliminaron los sensores ANT+ - Se han eliminado los sensores ANT+
- Se eliminaron los sensores clásicos de Bluetooth. - Se han eliminado los sensores Bluetooth clásicos
- Se eliminó Protobuf - Se ha eliminado Protobuf
- Se eliminaron la estimación de calorías y la estimación de actividad. - Se han eliminado la estimación de calorías y la estimación de actividad
- Se eliminó la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones) - Se ha eliminado la API del servicio MyTracks (es decir, no hay integración con otras aplicaciones)
@@ -13,5 +13,5 @@ Correcciones de errores:
- Se ha corregido la exportación a KML/KMZ - Se ha corregido la exportación a KML/KMZ
Desarrollador: Desarrollador:
- Las pruebas son ejecutables (pero muchas fallan) - Las pruebas se pueden ejecutar (aunque muchas fallan)
- Se ha habilitado la integración continua con TravisCI (sin pruebas) - Se ha habilitado la integración continua con TravisCI (sin pruebas)
@@ -1,14 +1,14 @@
v3.1.0: OpenTracks v3.1.0: OpenTracks
Cambios: Cambios:
- Se agregó la capacidad de mostrar rutas en la aplicación de mapas (probado con OsmAnd y Maps.ME). - Se ha añadido la opción de mostrar el recorrido en aplicaciones de mapas (probado con OsmAnd y Maps.ME)
- Se ha añadido la posibilidad de visualizar las estadísticas de seguimiento sin desbloquear el dispositivo durante el mismo. - Se ha añadido la opción de mostrar las estadísticas del recorrido sin desbloquear el dispositivo mientras se realiza el seguimiento
- Se ha eliminado el modo horizontal de la vista de detalles de la pista (debido a un error). - Se ha eliminado el modo horizontal para la vista detallada del recorrido (presentaba errores)
- El historial de sugerencias de búsqueda ha sido eliminado. - Se ha eliminado el historial de sugerencias de búsqueda
Corrección de errores: Corrección de errores:
- Solicitar permisos de almacenamiento para todas las importaciones/todas las exportaciones - Se solicita permiso de almacenamiento para «Importar todo»/«Exportar todo»
Desarrolladores: Desarrollador:
- Actualizar a Gradle 5.6 - Actualización a Gradle 5.6
- No minimice la versión de lanzamiento - No se minimizan las compilaciones de lanzamiento
@@ -0,0 +1,7 @@
v3.2.1: OpenTracks
Cambios:
- Los ajustes ahora están en una sola página
Desarrollo:
-Los ajustes se migraron a Androidx compat (usa fragments)
@@ -0,0 +1,13 @@
v3.2.2: OpenTracks
Cambios:
- Se eliminaron las estadísticas agregadas
- Migrado a Material Design
Corrección de errores:
- Se corrigió la exportación de datos de sensores
- Se corrigió la importación de datos de sensores incompletos
Desarrollador:
- Se reemplazó la barra de acción (actionbar) por la barra de herramientas (toolbar)
- Migrado a androidx appcompat (incl. Material Design)
@@ -0,0 +1,14 @@
v3.2.3: OpenTracks
Cambios:
- Compartir: usar Android Sharesheet
- Compartir: compartir múltiples pistas como archivos individuales
- Usar Android ShareSheet
- Sensor de frecuencia cardíaca (Bluetooth LE): reconexión automática
Corrección de errores:
- Sensor de frecuencia cardíaca (Bluetooth LE): desconexión correcta
- La frecuencia cardíaca no siempre se mostraba durante la grabación
Desarrollador:
- Se habilitaron las pruebas de instrumentación en TravisCI
@@ -0,0 +1,5 @@
v3.2.4: OpenTracks
Corrección de errores:
- Caída/cierre inesperado (crash) en la actividad de ajustes
Se eliminó la posibilidad de cambiar la frecuencia de voz/fracción desde el menú contextual
@@ -0,0 +1,15 @@
v3.2.5: OpenTracks
Cambios:
- Deshabilitar el cambio de actividad
- Añadir permiso para Android 10: grabación de datos de ubicación
Corrección de errores:
- Corregir el restablecimiento de los ajustes
Desarrollador:
- Preferencias
- si no se pueden cargar los Ajustes, restablecer todas las preferencias
- se movió el acceso a las preferencias a PreferencesUtil
- se movieron los valores por defecto de las preferencias del código a XML
- usar SummaryProvider
@@ -0,0 +1,8 @@
v3.2.6: OpenTracks
Cambios:
- Monitor de frecuencia cardíaca: escanear dispositivos disponibles
- Eliminar acceso directo a los ajustes de Bluetooth
Desarrollador:
- Preferencias: se movieron los valores por defecto de las preferencias del código a XML
@@ -0,0 +1,10 @@
v3.2.7: OpenTracks
Cambios:
- La notificación muestra la precisión del GPS
- Mostrar notificación emergente (heads-up) si la precisión del GPS cae por debajo del umbral
- Mostrar siempre el gráfico tanto por tiempo como por distancia
- Se añadió ayuda (en Ajustes)
Corrección de errores:
- TrackDetailActivity: se deshabilitó el modo horizontal que no funcionaba (temporal)
@@ -0,0 +1,10 @@
v3.2.8: OpenTracks
Cambios:
- Gráfico: zoom mediante gestos
Corrección de errores:
- La exportación a través de KMZ no exportaba correctamente los nombres de archivo de las imágenes
- Compartir múltiples pistas no incluía los archivos KMZ
- No se mostraba la interfaz de usuario de MarkerDetailActivity
- Los ajustes se cerraban inesperadamente si la preferencia de frecuencia cardíaca no estaba inicializada
@@ -0,0 +1,10 @@
v3.2.9: OpenTracks
Cambios:
- Opción de mantener la pantalla encendida durante la grabación
- Marcadores: se eliminaron por completo los marcadores de división (split markers)
- Los anuncios de voz ahora funcionan sin el permiso READ_PHONE_STATE
Corrección de errores:
- Estadísticas: no se mostraba la frecuencia cardíaca
- Estadísticas: el tiempo se restablecía brevemente a 0s
@@ -0,0 +1,11 @@
v3.2.10: OpenTracks
Cambios:
- ChartView ya no es configurable
- Después de reiniciar: se eliminó el reinicio automático de una grabación incompleta
Corrección de errores:
- Los anuncios de voz usan MAY_DUCK (de nuevo)
- ChartView muestra marcadores (de nuevo)
- Se corrigió el diseño de grabación en horizontal (landscape recordingLayout) de TrackListActivity
- Se solicita el comportamiento heredado de la API de archivos (necesario para Android 10)
@@ -0,0 +1,12 @@
v3.2.11: OpenTracks
Error:
- La velocidad máxima es idéntica a la velocidad promedio
Corrección de errores:
- Cierre inesperado (crash) al seleccionar el sensor de frecuencia cardíaca
Desarrollador:
- Se reemplazó TabHost por TabsLayout
- Se eliminó el primer punto de ruta (waypoint) especial
- Se eliminaron las traducciones duplicadas
@@ -0,0 +1,13 @@
v3.3.0: OpenTracks
Cambios importantes:
- Fotos en almacenamiento privado (ocultas para otras apps)
NOTA: No se verán fotos de versiones anteriores
- Español (Latinoamérica) requiere Android 7.0 o superior
Cambios:
- Importar/exportar vía Storage Access Framework (sin permisos de almacenamiento)
Desarrollador:
- Limpieza de estructuras de datos internas
- Icono/nombre distintos para versión debug
@@ -0,0 +1,11 @@
v3.3.1: OpenTracks
Cambios:
- Android 10: permite conservar datos al desinstalar
- API Dashboard: icono de mapa para abrir la pista en app externa (ej. OSMDashboard)
Corrección de errores:
- Se corrigió el nombre de la app (roto en v3.3.0)
Desarrollador:
- Documentación de la API Dashboard en README_DEVELOPER.md
@@ -0,0 +1,11 @@
v3.3.2: OpenTracks
Cambios:
- Exportación: ahora sobrescribe los archivos
Corrección de errores:
- Se elimina siempre la notificación de grabación
Desarrollador:
- En API 26+ se usa java.time para analizar y escribir fechas
- Refactorización de TrackRecordingService
@@ -0,0 +1,12 @@
v3.4.0: OpenTracks
Cambios:
- API Dashboard: múltiples pistas
- Ayuda actualizada
Corrección de errores:
- Se corrigió el cierre inesperado al recrear fragmentos
- En pausa: evita que la app se detenga
Desarrollador:
- Pruebas habilitadas vía CI (Github Actions y TravisCI)
@@ -0,0 +1,14 @@
v3.5.0: OpenTracks
Cambios:
- Marcador: seleccionar imágenes de la galería
- Marcador: compartir imagen
- Utilizar el nombre de la ruta como nombre de archivo al compartir
- Se han eliminado los valores mínimos y máximos de altitud (eran imprecisos)
Corrección de errores:
- Los gráficos utilizan las unidades y la escala preferidas
Desarrollador:
- Base de datos: eliminar columnas en desuso
- Base de datos: añadir índices
@@ -0,0 +1,10 @@
v3.6.0: OpenTracks
Cambios:
- Ahora se pueden reanudar las rutas que se habían detenido
Corrección de errores:
- La importación de archivos KMZ no procesaba correctamente la URL de las fotos
Desarrollador:
- Se ha simplificado el procesamiento de datos
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Corrección de errores:
- Importación de KMZ: guardar la ruta correcta de la imagen
@@ -0,0 +1,4 @@
v3.6.2: OpenTracks
Corrección de errores:
- Los gráficos no se mostraban correctamente (problema introducido en la v3.6.0)
@@ -0,0 +1,13 @@
v3.7.0: OpenTracks
Cambios:
- Informe de velocidad/ritmo por tipo de actividad
- Se ha añadido una solución provisional para el cambio de semana del GPS
- (Experimental) Compatibilidad con sensores de ciclismo Bluetooth LE (cadencia y velocidad)
- Se ha mejorado la importación de KMZ/KML
Corrección de errores:
- Importación de KMZ: solo se importan las imágenes utilizadas
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,7 @@
v3.7.1: OpenTracks
Solución provisional:
- Ocultar el botón de inicio del GPS
Corrección de errores:
- Se ha corregido un puntero nulo
@@ -0,0 +1,6 @@
v3.7.2: OpenTracks
Corrección de errores:
- Se ha vuelto a añadir el botón «Iniciar GPS»
- El nombre de la ruta ya no se sobrescribe al final de una grabación
- Marcadores: al eliminar una foto, también se borra el archivo
@@ -0,0 +1,7 @@
v3.7.3: OpenTracks
Corrección de errores:
- Se ha corregido el cálculo de la cadencia y la velocidad (mediante el sensor Bluetooth LE)
Desarrollador:
- Se han eliminado las traducciones que no se utilizaban
@@ -0,0 +1,12 @@
v3.7.4: OpenTracks
Cambios:
- Formato de compartir/exportar configurable
Corrección de errores:
- El diámetro de la rueda no se almacenaba correctamente (para el sensor de velocidad)
- Ayuda: los enlaces son clicables
Desarrollador:
- Se ha dividido TrackDetailActivity
- El sucesor de StatsFragment utiliza ConstraintLayout
@@ -0,0 +1,11 @@
v3.7.5: OpenTracks
Cambios:
- Se ha añadido un resumen de estadísticas agregadas (desde el menú de la pantalla principal)
- API del panel de control: se pasan los ajustes «mostrar en la pantalla de bloqueo» y «mantener la pantalla encendida»
- Se han actualizado las traducciones
- Importación: se ha flexibilizado el formato de fecha ISO 8601
- Mostrar ruta en el mapa: se puede seleccionar el formato de archivo y la apertura del panel de control funciona en Android R (API 30+)
Corrección de errores:
- El icono de la aplicación ahora se muestra correctamente
@@ -0,0 +1,11 @@
v3.7.6: OpenTracks
Cambios:
- Modo horizontal
- La interfaz de usuario para borrar rutas ahora es solo un indicador giratorio
- La velocidad se exporta e importa (GPX/KML/KMZ)
- El formato de exportación predeterminado es KMZ, incluyendo todos los datos y las imágenes
Desarrollador:
- Se han sustituido las AsyncTasks por hilos
- Sincronización: se genera un UUID por cada ruta (aún no se exporta)
@@ -0,0 +1,16 @@
v3.8.0: OpenTracks
Cambio importante:
- Importación/exportación de GPX de velocidad, cadencia y frecuencia cardíaca a través de TrackPointExtension/v2
Cambios:
- API del panel de control: permite acceder a los marcadores (incluida una llamada de retorno para mostrarlos en OpenTracks)
- Nuevo icono de marcador (chincheta naranja, SVG)
- Los archivos que se van a importar se pueden seleccionar a través del gestor de archivos
Correcciones de errores:
- La interfaz de usuario de estadísticas agregadas respeta la frecuencia preferida
- Fallaba la edición de un marcador al cambiar la orientación
Desarrollador:
- Se ha refactorizado TrackRecordingService
@@ -0,0 +1,8 @@
v3.8.1: OpenTracks
Cambios:
- Durante la grabación: se ha añadido la opción de pantalla completa
- El panel de control se inicia en la misma tarea
Correcciones de errores:
- Los filtros de intención para importar archivos eran demasiado optimistas
@@ -0,0 +1,14 @@
v3.9.0: OpenTracks
Cambios:
- Exportación de rutas con su UUID
- Se evita la importación de rutas ya existentes por su UUID (configurable; por defecto, activado)
- Ahora hay que mantener pulsado el botón de pausa/parada
- El preinicio del GPS muestra el estado de la señal
- Se han añadido nuevos iconos de actividad
- Sensor de velocidad: ahora se puede configurar el tamaño de la rueda en milímetros
Correcciones de errores:
- Se han corregido algunos fallos poco frecuentes (NPE)
- Se ha corregido el formato del ritmo
- Se ha cambiado la posición del icono de la aplicación en el lanzador
@@ -1,7 +1,7 @@
v4.14.1: OpenTracks v4.14.1: OpenTracks
Cambios: Cambios:
- Detección de inactividad se puede desactivar - Se puede desactivar la detección de inactividad
Correcciones de errores: Corrección de errores:
- UI para añadir imágenes a los marcadores estaba oculto - La interfaz de usuario para añadir imágenes a los marcadores estaba oculta
@@ -1,8 +1,22 @@
OpenTracks es un tracker deportivo que respeta tu privacidad. Un compañero de seguimiento deportivo que respeta tu privacidad.
Funciones: El deporte y las actividades al aire libre aportan felicidad, destreza y confianza.
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
* Marca lugares de interés, incluso agrega fotos a los marcadores Cuida tu salud llevando un registro de tu entrenamiento.
* Pulsómetro (compatible con dispositivos Bluetooth LE) Registra tus sesiones de carrera o caminata, y te ofrece un ciclocomputador con una pantalla más grande para el ciclismo.
* Exporta tus rutas como KML o GPX Marca lugares interesantes a lo largo de tu recorrido con fotos.
* Completamente offline, no necesitas internet Guarda estadísticas detalladas para su análisis.
Comparte solo los datos que quieras que vean los demás.
* Avisos de voz.
* Compatible con sensores Bluetooth LE: frecuencia cardíaca, velocidad y distancia (ciclismo), cadencia (ciclismo) y medidor de potencia (ciclismo).
* Desnivel positivo y negativo: mediante sensor barométrico.
* Altitud mostrada en EGM2008 (por encima del nivel medio del mar); exportada como WGS84.
* Exporta los datos como rutas en formato KMZ (incluidas fotos), KML o GPX.
* No requiere acceso a Internet ni permisos adicionales.
* Temas oscuro y claro, respetando la configuración del sistema.
* Sin anuncios.
Software libre / Software gratuito / Código abierto,
lo que significa que puedes utilizar, estudiar, modificar y compartir el código fuente.
Licencia Apache 2.0
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
Rilisan pertama OpenTracks (fork keras dari MyTracks)
Perubahan:
- Perubahan merek MyTracks ke OpenTracks
- Menambahkan sensor detak jantung Bluetooth LE
- Menghapus semua fitur yang memerlukan koneksi internet
- Menghapus integrasi Google Maps
- Menghapus penggunaan layanan Google Play
- Menghapus sensor ANT+
- Menghapus sensor klasik Bluetooth
- Menghapus Protobuf
- Menghapus estimasi kalori dan aktivitas
- Menghapus API MyTracks Service (dengan kata lain, tidak ada integrasi dengan aplikasi lain)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Perubahan:
- Bagikan penanda
- Perubahan struktur pengaturan
- Menghapus tombol kembali di bilah aksi
- Ekspor: menghapus pilihan pengaturan ukuran foto
- Ekspor: KML sebagai format sekarang dapat dipilih
Perbaikan Bug:
- direktori ekspor sekarang di /OpenTracks/
- mode lanskap tidak akan mogok
- memperbaiki ekspor KML/KMZ
Pengembang:
- Tes dapat dieksekusi (namun banyak yang gagal)
- Mengaktifkan integrasi terus menerus dengan TravisCI (tanpa tes)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Perubahan:
- Menambahkan opsi tampilan jalur salju di peta aplikasi (telah diuji dengan OsmAnd dan Maps.ME)
- Menambahkan ketika melacak, akan menampilkan statistik jalur tanpa perlu membuka kunci perangkat
- Menghapus mode landkap untuk tampilan detail jalur (tidak stabil)
- Menghapus riwayat saran penelusuran
Perbaikan Bug:
- Permintaan izin penyimpanan untuk impor/ekspor semua
Pengembang:
- Pembaruan ke Gradle 5.6
- Jangan mengecilkan (minify) rilisan build
@@ -1,4 +1,4 @@
Teman pelacakan olahraga yang menghormati privasi Anda. Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
Olahraga dan aktivitas luar ruangan memberikan kebahagiaan, keterampilan, dan kepercayaan diri. Olahraga dan aktivitas luar ruangan memberikan kebahagiaan, keterampilan, dan kepercayaan diri.
@@ -11,12 +11,12 @@ Hanya membagikan data yang Anda ingin orang lain memilikinya.
* Notifikasi suara. * Notifikasi suara.
* Mendukung sensor Bluetooth LE: detak jantung, kecepatan dan jarak (bersepeda), irama (bersepeda), dan pengukur daya (bersepeda). * Mendukung sensor Bluetooth LE: detak jantung, kecepatan dan jarak (bersepeda), irama (bersepeda), dan pengukur daya (bersepeda).
* Kenaikan dan penurunan ketinggian: melalui sensor barometrik. * Kenaikan dan penurunan ketinggian: melalui sensor barometrik.
* Ketinggian yang ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84. * Ketinggian ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Mengekspor data sebagai lintasan, baik sebagai KMZ (termasuk foto), KML, atau GPX. * Mengekspor data sebagai lintasan, baik sebagai KMZ (termasuk foto), KML, atau GPX.
* Tidak ada akses Internet atau izin tambahan. * Tidak ada akses Internet atau izin tambahan.
* Tema gelap dan terang, dengan memperhatikan pengaturan sistem. * Tema gelap dan terang, dengan memperhatikan pengaturan sistem.
* Tanpa iklan. * Tanpa iklan.
Perangkat lunak bebas / Perangkat Lunak Bebas / Sumber Terbuka Perangkat Lunak Bebas / Sumber Terbuka
yang berarti Anda dapat menggunakan, mempelajari, mengubah, dan berbagi kode sumber. yang berarti Anda dapat menggunakan, mempelajari, mengubah, dan berbagi kode sumber.
Berlisensi Apache 2.0 Berlisensi Apache 2.0
@@ -1 +1 @@
Sebuah pelacak olahraga yang benar-benar menghormati privasi Anda. Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
@@ -1 +1 @@
一個尊重您的隱私的運動蹤跡紀錄器。 一款尊重您隱私的運動蹤跡紀錄器。
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
@@ -0,0 +1,78 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ActivityTypeTest {
@Test
public void findBy_id() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running"));
}
@Test
public void findBy_idOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
assertEquals(activityType, ActivityType.findBy(activityType.getId()));
}
}
@Test
public void findBy_synonymsOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
for (String synonym : activityType.synonyms) {
assertEquals(activityType, ActivityType.findBy(synonym));
assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_')));
}
}
}
@Test
public void findBy_ignoresCase() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING"));
assertEquals(ActivityType.ATV, ActivityType.findBy("atv"));
}
@Test
public void findBy_ignoresSeparators() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking"));
assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing"));
}
@Test
public void findBy_ignoresSurroundingAndRepeatedWhitespace() {
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking "));
}
@Test
public void findBy_synonym() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill"));
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE"));
assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER"));
assertEquals(ActivityType.BIKING, ActivityType.findBy("bike"));
}
@Test
public void findBy_unknown() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft"));
}
@Test
public void findBy_null() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null));
}
@Test
public void findBy_blank() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" "));
}
}
@@ -120,6 +120,184 @@ public class GPXTrackImporterTest {
), importedTrackPoints); ), importedTrackPoints);
} }
@LargeTest
@Test
public void gpx_missing_time() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_missing_time);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("20210907_213924.gpx", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackstatistics
Statistics trackStatistics = importedTrack.statistics();
assertEquals(0, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(0), trackStatistics.movingDuration());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(1, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null))
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_pwd() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_pwr);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_gpxpx() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_gpxpx);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest @LargeTest
@Test @Test
public void gpx_speed_no_namespace() throws IOException { public void gpx_speed_no_namespace() throws IOException {
@@ -285,8 +463,8 @@ public class GPXTrackImporterTest {
public void importExportTest_timezone() throws IOException { public void importExportTest_timezone() throws IOException {
// given // given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter)); XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone); InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) { try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when // when
@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>20210907_213924.gpx</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time/>
<hdop>4</hdop>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -28,10 +28,11 @@ xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/
<opentracks:typeTranslated><![CDATA[unknown]]></opentracks:typeTranslated> <opentracks:typeTranslated><![CDATA[unknown]]></opentracks:typeTranslated>
<gpxtrkx:TrackStatsExtension> <gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance> <gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime> <gpxtrkx:TotalElapsedTime>0</gpxtrkx:TotalElapsedTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime> <gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime> <gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed> <gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:MovingSpeed>0.0</gpxtrkx:MovingSpeed>
</gpxtrkx:TrackStatsExtension> </gpxtrkx:TrackStatsExtension>
</extensions> </extensions>
<trkseg> <trkseg>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>100</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>110</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>150</gpxpx:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>100</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>110</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>150</pwr:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
+3
View File
@@ -71,6 +71,9 @@ limitations under the License.
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" /> <uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" /> <uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED" />
<uses-permission android:name="android.permission.health.WRITE_SPEED" />
<application <application
android:name=".Startup" android:name=".Startup"
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -14,13 +15,13 @@ public enum ActivityType {
AIRPLANE("airplane", R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp, true), AIRPLANE("airplane", R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp, true),
ATV("ATV", R.string.activity_type_atv, R.drawable.ic_activity_drive_24dp, true), ATV("ATV", R.string.activity_type_atv, R.drawable.ic_activity_drive_24dp, true),
BIKING("biking", R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true), BIKING("biking", List.of("bike", "indoor cycling"), R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.ICON_UNKNOWN, true), BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.ICON_UNKNOWN, true),
BOAT("boat", R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp, true), BOAT("boat", R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp, true),
CLIMBING("climbing", R.string.activity_type_climbing, R.drawable.ic_activity_climbing_24dp, false), CLIMBING("climbing", R.string.activity_type_climbing, R.drawable.ic_activity_climbing_24dp, false),
COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, R.drawable.ic_activity_flight_24dp, true), COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, R.drawable.ic_activity_flight_24dp, true),
CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, R.drawable.ic_activity_skiing_24dp, true), CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, R.drawable.ic_activity_skiing_24dp, true),
CYCLING("cycling", R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true), CYCLING("cycling", List.of("cycle"), R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true),
DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, R.drawable.ic_activity_mtb_24dp, true), DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, R.drawable.ic_activity_mtb_24dp, true),
DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.ICON_UNKNOWN, true), DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.ICON_UNKNOWN, true),
DRIVING("driving", R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp, true), DRIVING("driving", R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp, true),
@@ -30,21 +31,21 @@ public enum ActivityType {
FERRY("ferry", R.string.activity_type_ferry, R.drawable.ic_activity_boat_24dp, true), FERRY("ferry", R.string.activity_type_ferry, R.drawable.ic_activity_boat_24dp, true),
FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.ICON_UNKNOWN, true), FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.ICON_UNKNOWN, true),
GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.ICON_UNKNOWN, true), GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.ICON_UNKNOWN, true),
GRAVEL_BIKING("gravel biking", R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true), GRAVEL_BIKING("gravel biking", List.of("gravel bike"), R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true),
HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.ICON_UNKNOWN, true), HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.ICON_UNKNOWN, true),
HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.ICON_UNKNOWN, true), HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.ICON_UNKNOWN, true),
HIKING("hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false), HIKING("hiking", List.of("hike"), R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true), HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.ICON_UNKNOWN, true), HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.ICON_UNKNOWN, true),
ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.ICON_UNKNOWN, true), ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.ICON_UNKNOWN, true),
INLINE_SKATING("inline skating", R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true), INLINE_SKATING("inline skating", List.of("inline skate"), R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true),
KAYAKING("kayaking", R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp, true), KAYAKING("kayaking", R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp, true),
KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.ICON_UNKNOWN, true), KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.ICON_UNKNOWN, true),
LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.ICON_UNKNOWN, true), LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.ICON_UNKNOWN, true),
MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.ICON_UNKNOWN, true), MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.ICON_UNKNOWN, true),
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp, true), MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp, true),
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true), MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true),
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true), MOUNTAIN_BIKING("mountain biking", List.of("mountain bike"), R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false), OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true), OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true),
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true), PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true),
@@ -53,46 +54,57 @@ public enum ActivityType {
RC_BOAT("RC boat", R.string.activity_type_rc_boat, R.drawable.ic_activity_boat_24dp, true), RC_BOAT("RC boat", R.string.activity_type_rc_boat, R.drawable.ic_activity_boat_24dp, true),
RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.ICON_UNKNOWN, true), RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.ICON_UNKNOWN, true),
RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true), RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
ROAD_BIKING("road biking", R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true), ROAD_BIKING("road biking", List.of("road bike"), R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true),
ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.ICON_UNKNOWN, true), ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.ICON_UNKNOWN, true),
ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.ICON_UNKNOWN, true), ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.ICON_UNKNOWN, true),
RUNNING("running", R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false), RUNNING("running", List.of("run", "treadmill"), R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false),
SAILING("sailing", R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp, true), SAILING("sailing", R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp, true),
KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, R.drawable.ic_activity_scooter_24dp, true), KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, R.drawable.ic_activity_scooter_24dp, true),
SEAPLANE("seaplane", R.string.activity_type_seaplane, R.drawable.ic_activity_flight_24dp, true), SEAPLANE("seaplane", R.string.activity_type_seaplane, R.drawable.ic_activity_flight_24dp, true),
SKATE_BOARDING("skateboarding", R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true), SKATE_BOARDING("skateboarding", List.of("skateboard"), R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true),
SKATING("skating", R.string.activity_type_skating, ActivityIcon.ICON_UNKNOWN, true), SKATING("skating", R.string.activity_type_skating, ActivityIcon.ICON_UNKNOWN, true),
SKIING("skiing", R.string.activity_type_skiing, R.drawable.ic_activity_skiing_24dp, true), SKIING("skiing", R.string.activity_type_skiing, R.drawable.ic_activity_skiing_24dp, true),
SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.ICON_UNKNOWN, true), SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.ICON_UNKNOWN, true),
SLED("sled", R.string.activity_type_sled, ActivityIcon.ICON_UNKNOWN, true), SLED("sled", R.string.activity_type_sled, ActivityIcon.ICON_UNKNOWN, true),
SNOW_BOARDING("snowboarding", R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true), SNOW_BOARDING("snowboarding", List.of("snowboard"), R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true),
SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.ICON_UNKNOWN, true), SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.ICON_UNKNOWN, true),
SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, R.drawable.ic_activity_walk_24dp, false), SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, R.drawable.ic_activity_walk_24dp, false),
STREET_RUNNING("street running", R.string.activity_type_street_running, R.drawable.ic_activity_run_24dp, false), STREET_RUNNING("street running", R.string.activity_type_street_running, R.drawable.ic_activity_run_24dp, false),
SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.ICON_UNKNOWN, true), SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.ICON_UNKNOWN, true),
TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, R.drawable.ic_activity_bike_24dp, true), TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, R.drawable.ic_activity_bike_24dp, true),
TRACK_RUNNING("track running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), TRACK_RUNNING("track running", List.of("track run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, R.drawable.ic_activity_walk_24dp, false), TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
TRAIL_RUNNING("trail running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), TRAIL_RUNNING("trail running", List.of("trail run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIN("train", R.string.activity_type_train, ActivityIcon.ICON_UNKNOWN, true), TRAIN("train", R.string.activity_type_train, ActivityIcon.ICON_UNKNOWN, true),
ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.ICON_UNKNOWN, true), ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.ICON_UNKNOWN, true),
WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.ICON_UNKNOWN, true), WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.ICON_UNKNOWN, true),
WALKING("walking", R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false), WALKING("walking", List.of("walk"), R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false),
WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.ICON_UNKNOWN, true), WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.ICON_UNKNOWN, true),
WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.ICON_UNKNOWN, true), WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.ICON_UNKNOWN, true),
SWIMMING("swimming", R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false), SWIMMING("swimming", List.of("swim"), R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false),
SWIMMING_OPEN("swimming in open water", R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false), SWIMMING_OPEN("swimming in open water", List.of("swimming openwater", "open water swimming"), R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false),
WORKOUT("workout", R.string.activity_type_workout, R.drawable.ic_activity_workout_24dp, false), WORKOUT("workout", R.string.activity_type_workout, R.drawable.ic_activity_workout_24dp, false),
UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.ICON_UNKNOWN, true); UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.ICON_UNKNOWN, true);
final String id; final String id;
/**
* Spellings used by other applications that {@link #normalize(String)} cannot map onto {@link #id}.
* Already normalized.
*/
final List<String> synonyms;
final int iconDrawableId; final int iconDrawableId;
final boolean showSpeedPreferred; final boolean showSpeedPreferred;
final int localizedStringId; final int localizedStringId;
ActivityType(String id, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) { ActivityType(String id, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this(id, List.of(), localizedStringId, iconDrawableId, showSpeedPreferred);
}
ActivityType(String id, List<String> synonyms, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this.id = id; this.id = id;
this.synonyms = synonyms;
this.localizedStringId = localizedStringId; this.localizedStringId = localizedStringId;
this.iconDrawableId = iconDrawableId; this.iconDrawableId = iconDrawableId;
this.showSpeedPreferred = showSpeedPreferred; this.showSpeedPreferred = showSpeedPreferred;
@@ -121,12 +133,39 @@ public enum ActivityType {
.toList(); .toList();
} }
/**
* Lowercases and reduces "_", "-" and whitespace runs to a single space.
* Lowercasing is locale-independent: under a Turkish locale the I of "RUNNING" would otherwise
* lowercase to a dotless i and stop matching "running".
*/
@NonNull
private static String normalize(String activityTypeId) {
if (activityTypeId == null) {
return "";
}
return activityTypeId.toLowerCase(Locale.ROOT)
.replaceAll("[\\s_-]+", " ")
.trim();
}
/**
* Resolves an activity type as written by another application; GPX's type element is free text.
*/
@NonNull @NonNull
public static ActivityType findBy(String activityTypeId) { public static ActivityType findBy(String activityTypeId) {
return Arrays.stream(ActivityType.values()).filter( String normalized = normalize(activityTypeId);
it -> it.id.equals(activityTypeId) if (normalized.isEmpty()) {
).findFirst() return ActivityType.UNKNOWN;
.orElse(ActivityType.UNKNOWN); }
return Arrays.stream(ActivityType.values())
.filter(it -> normalize(it.id).equals(normalized))
.findFirst()
.orElseGet(() -> Arrays.stream(ActivityType.values())
.filter(it -> it.synonyms.contains(normalized))
.findFirst()
.orElse(ActivityType.UNKNOWN));
} }
@NonNull @NonNull
@@ -105,7 +105,7 @@ public class ExportActivity extends AbstractActivity {
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported. // List of tracks to be exported.
private ArrayList<ExportTask> exportTasks; private final ArrayList<ExportTask> exportTasks = new ArrayList<>();
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>(); private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler(); private final Handler conflictsHandler = new Handler();
@@ -183,7 +183,7 @@ public class ExportActivity extends AbstractActivity {
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT); trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES); directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS); trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS)); exportTasks.addAll(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
setProgress(); setProgress();
nextExport(null); nextExport(null);
} }
@@ -226,7 +226,6 @@ public class ExportActivity extends AbstractActivity {
return false; return false;
} }
exportTasks = new ArrayList<>();
if (allInOneFile) { if (allInOneFile) {
String filename = "OpenTracks-Backup"; String filename = "OpenTracks-Backup";
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::id).toList())); exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::id).toList()));
@@ -282,10 +282,11 @@ public class GPXTrackExporter implements TrackExporter {
Statistics trackStatistics = track.statistics(); Statistics trackStatistics = track.statistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>"); printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.totalDistance().toM() + "</gpxtrkx:Distance>"); printWriter.println("<gpxtrkx:Distance>" + trackStatistics.totalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TimerTime>"); printWriter.println("<gpxtrkx:TotalElapsedTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TotalElapsedTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.movingDuration().getSeconds() + "</gpxtrkx:MovingTime>"); printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.movingDuration().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>"); printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.maxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>"); printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.maxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
printWriter.println("<gpxtrkx:MovingSpeed>" + trackStatistics.getAverageMovingSpeed().toMPS() + "</gpxtrkx:MovingSpeed>");
if (trackStatistics.altitudeGainLoss() != null) { if (trackStatistics.altitudeGainLoss() != null) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.altitudeGainLoss().gain_m() + "</gpxtrkx:Ascent>"); printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.altitudeGainLoss().gain_m() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.altitudeGainLoss().loss_m() + "</gpxtrkx:Descent>"); printWriter.println("<gpxtrkx:Descent>" + trackStatistics.altitudeGainLoss().loss_m() + "</gpxtrkx:Descent>");
@@ -18,8 +18,11 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.Locator; import org.xml.sax.Locator;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
@@ -71,28 +74,44 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_TRACK_POINT = "trkpt"; private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg"; private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type"; private static final String TAG_TYPE = "type";
private static final String TAG_TYPE_LOCALIZED = "opentracks:typeTranslated";
private static final String TAG_MARKER = "wpt"; private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat"; private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon"; private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
/** /**
* Often speed is exported without the proper namespace. * Often speed is exported without the proper namespace.
*/ */
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed"; private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
private static final String TAG_EXTENSION_GAIN = "opentracks:gain"; /**
private static final String TAG_EXTENSION_LOSS = "opentracks:loss"; * Garmin Trackpoint Extensions.
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance"; */
private static final String TAG_EXTENSION_ACCURACY_HORIZONTAL = "opentracks:accuracy_horizontal"; private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
private static final String TAG_EXTENSION_ACCURACY_VERTICAL = "opentracks:accuracy_vertical"; private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V2 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v2";
private static final String TAG_GARMIN_TPX_EXTENSION_SPEED = "speed";
private static final String TAG_GARMIN_TPX_EXTENSION_HEARTRATE = "hr";
private static final String TAG_GARMIN_TPX_EXTENSION_TEMPERATURE = "atemp";
private static final String TAG_GARMIN_TPX_EXTENSION_CADENCE = "cad";
/**
* Garmin Trackpoint Extensions.
*/
private static final String NS_GARMIN_POWER_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/PowerExtension/v1";
private static final String TAG_GARMIN_PWR_EXTENSION_POWER = "PowerInWatts";
/**
* OpenTracks Extensions.
*/
private static final String NS_OPENTRACKS = "http://opentracksapp.com/xmlschemas/v1";
private static final String TAG_OPENTRACKS_EXTENSION_TRACK_ID = "trackid";
private static final String TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED = "typeTranslated";
private static final String TAG_OPENTRACKS_EXTENSION_GAIN = "gain";
private static final String TAG_OPENTRACKS_EXTENSION_LOSS = "loss";
private static final String TAG_OPENTRACKS_EXTENSION_DISTANCE = "distance";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL = "accuracy_horizontal";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL = "accuracy_vertical";
private Locator locator; private Locator locator;
private final Context context; private final Context context;
@@ -142,8 +161,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) { public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (tag) { switch (qName) {
case TAG_MARKER: case TAG_MARKER:
onMarkerStart(attributes); onMarkerStart(attributes);
break; break;
@@ -165,105 +184,128 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
@Override @Override
public void endElement(String uri, String localName, String tag) { public void endElement(String uri, String localName, String qName) {
switch (tag) { if (NS_GARMIN_TRACKPOINT_EXTENSION_V1.equals(uri) || NS_GARMIN_TRACKPOINT_EXTENSION_V2.equals(uri)) {
case TAG_GPX -> onFileEnd(); switch (localName) {
case TAG_MARKER -> onMarkerEnd(); case TAG_GARMIN_TPX_EXTENSION_SPEED -> {
case TAG_TRACK -> { if (content != null) {
if (activityTypeLocalized == null) { speed = content.trim();
// Backward compatibility: up v4.9.1 as <type> contained localized content. }
activityTypeLocalized = activityType;
} }
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset); case TAG_GARMIN_TPX_EXTENSION_HEARTRATE -> {
zoneOffset = null; if (content != null) {
} heartrate = content.trim();
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd(); }
case TAG_TRACK_POINT -> currentSegment.add(createTrackPoint());
case TAG_NAME -> {
if (content != null) {
name = content.trim();
} }
} case TAG_GARMIN_TPX_EXTENSION_TEMPERATURE -> {
case TAG_DESCRIPTION -> { if (content != null) {
if (content != null) { temperature = content.trim();
description = content.trim(); }
} }
} case TAG_GARMIN_TPX_EXTENSION_CADENCE -> {
case TAG_TYPE -> { //Track or Marker/WPT if (content != null) {
if (content != null) { cadence = content.trim();
// In older version this might be localized content. }
activityType = content.trim();
markerType = content.trim();
} }
default -> Log.w(TAG, "Unknown Garmin trackpoint extension tag " + localName);
} }
case TAG_TYPE_LOCALIZED -> { } else if (NS_GARMIN_POWER_EXTENSION_V1.equals(uri)) {
if (content != null) { //noinspection SwitchStatementWithTooFewBranches
activityTypeLocalized = content.trim(); switch (localName) {
case TAG_GARMIN_PWR_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
}
} }
default -> Log.w(TAG, "Unknown Garmin power extension tag " + localName);
} }
case TAG_TIME -> { } else if (NS_OPENTRACKS.equals(uri)) {
if (content != null) { switch (localName) {
time = content.trim(); case TAG_OPENTRACKS_EXTENSION_TRACK_ID -> {
if (content != null) {
uuid = content.trim();
}
} }
} case TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED -> {
case TAG_ALTITUDE -> { if (content != null) {
if (content != null) { activityTypeLocalized = content.trim();
altitude = content.trim(); }
} }
} case TAG_OPENTRACKS_EXTENSION_GAIN -> {
case TAG_EXTENSION_SPEED, TAG_EXTENSION_SPEED_COMPAT -> { if (content != null) {
if (content != null) { gain = content.trim();
speed = content.trim(); }
} }
} case TAG_OPENTRACKS_EXTENSION_LOSS -> {
case TAG_EXTENSION_HEARTRATE -> { if (content != null) {
if (content != null) { loss = content.trim();
heartrate = content.trim(); }
} }
} case TAG_OPENTRACKS_EXTENSION_DISTANCE -> {
case TAG_EXTENSION_TEMPERATURE -> { if (content != null) {
if (content != null) { sensorDistance = content.trim();
temperature = content.trim(); }
} }
} case TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL -> {
case TAG_EXTENSION_CADENCE -> { if (content != null) {
if (content != null) { accuracyHorizontal = content.trim();
cadence = content.trim(); }
} }
} case TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL -> {
case TAG_EXTENSION_POWER -> { if (content != null) {
if (content != null) { accuracyVertical = content.trim();
power = content.trim(); }
} }
default -> Log.w(TAG, "Unknown OpenTracks extension tag " + localName);
} }
case TAG_ID -> { } else {
if (content != null) { switch (qName) {
uuid = content.trim(); case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
if (activityTypeLocalized == null) {
// Backward compatibility: up v4.9.1 as <type> contained localized content.
activityTypeLocalized = activityType;
}
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset);
zoneOffset = null;
} }
} case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_EXTENSION_GAIN -> { case TAG_TRACK_POINT -> {
if (content != null) { TrackPoint trackPoint = createTrackPoint();
gain = content.trim(); if (trackPoint != null) currentSegment.add(trackPoint);
} }
} case TAG_NAME -> {
case TAG_EXTENSION_LOSS -> { if (content != null) {
if (content != null) { name = content.trim();
loss = content.trim(); }
} }
} case TAG_DESCRIPTION -> {
case TAG_EXTENSION_DISTANCE -> { if (content != null) {
if (content != null) { description = content.trim();
sensorDistance = content.trim(); }
} }
} case TAG_TYPE -> { //Track or Marker/WPT
case TAG_EXTENSION_ACCURACY_HORIZONTAL -> { if (content != null) {
if (content != null) { // In older version this might be localized content.
accuracyHorizontal = content.trim(); activityType = content.trim();
markerType = content.trim();
}
} }
} case TAG_TIME -> {
case TAG_EXTENSION_ACCURACY_VERTICAL -> { if (content != null) {
if (content != null) { time = content.trim();
accuracyVertical = content.trim(); }
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
}
}
case TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
}
} }
} }
} }
@@ -284,8 +326,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
currentSegment.clear(); currentSegment.clear();
} }
@Nullable
private TrackPoint createTrackPoint() throws ParsingException { private TrackPoint createTrackPoint() throws ParsingException {
if (TextUtils.isEmpty(time)) return null;
OffsetDateTime parsedTime; OffsetDateTime parsedTime;
try { try {
parsedTime = StringUtils.parseTime(time); parsedTime = StringUtils.parseTime(time);
@@ -468,7 +512,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
TrackPoint trackPoint = createTrackPoint(); TrackPoint trackPoint = createTrackPoint();
if (trackPoint == null) {
Log.w(TAG, "Marker with no TrackPoint");
return;
}
if (!trackPoint.position().hasLocation()) { if (!trackPoint.position().hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position()); Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position());
return; return;
@@ -146,8 +146,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (tag) { switch (qName) {
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY -> case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, typeLocalized, and description. // Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, typeLocalized, and description.
onMarkerStart(); onMarkerStart();
@@ -168,8 +168,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
} }
@Override @Override
public void endElement(String uri, String localName, String tag) throws SAXException { public void endElement(String uri, String localName, String qName) throws SAXException {
switch (tag) { switch (qName) {
case TAG_KML -> onFileEnd(); case TAG_KML -> onFileEnd();
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY -> case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track. // Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track.
@@ -5,7 +5,10 @@ import android.content.Context;
import androidx.health.connect.client.HealthConnectClient; import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.PermissionController; import androidx.health.connect.client.PermissionController;
import androidx.health.connect.client.permission.HealthPermission; import androidx.health.connect.client.permission.HealthPermission;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseSessionRecord; import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.work.Data; import androidx.work.Data;
import androidx.work.ExistingWorkPolicy; import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest; import androidx.work.OneTimeWorkRequest;
@@ -27,11 +30,13 @@ import kotlinx.coroutines.BuildersKt;
*/ */
public class HealthConnectUtils { public class HealthConnectUtils {
static final String WRITE_EXERCISE_PERMISSION = HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)); public static final Set<String> PERMISSIONS = Set.of(
static final String WRITE_EXERCISE_ROUTE_PERMISSION = HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE; HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(DistanceRecord.class)),
//TODO Should be package private HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ElevationGainedRecord.class)),
public static final Set<String> PERMISSIONS = Set.of(WRITE_EXERCISE_PERMISSION, WRITE_EXERCISE_ROUTE_PERMISSION); HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(SpeedRecord.class)),
HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE
);
private HealthConnectUtils() { private HealthConnectUtils() {
} }
@@ -41,6 +46,9 @@ public class HealthConnectUtils {
} }
public static Set<String> getGrantedPermissions(Context context) { public static Set<String> getGrantedPermissions(Context context) {
if (!isAvailable(context))
return Set.of();
try { try {
HealthConnectClient client = HealthConnectClient.getOrCreate(context); HealthConnectClient client = HealthConnectClient.getOrCreate(context);
PermissionController controller = client.getPermissionController(); PermissionController controller = client.getPermissionController();
@@ -6,16 +6,22 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.health.connect.client.HealthConnectClient; import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseRoute; import androidx.health.connect.client.records.ExerciseRoute;
import androidx.health.connect.client.records.ExerciseSessionRecord; import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.Record;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.health.connect.client.records.metadata.Device; import androidx.health.connect.client.records.metadata.Device;
import androidx.health.connect.client.records.metadata.Metadata; import androidx.health.connect.client.records.metadata.Metadata;
import androidx.health.connect.client.units.Length; import androidx.health.connect.client.units.Length;
import androidx.health.connect.client.units.Velocity;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -111,27 +117,65 @@ public class HealthConnectWorker extends Worker {
throw new RuntimeException("Track not found: " + trackId); throw new RuntimeException("Track not found: " + trackId);
} }
ExerciseRoute exerciseRoute = createExerciseRoute(contentProviderUtils, trackId);
HealthConnectClient client = HealthConnectClient.getOrCreate(context); HealthConnectClient client = HealthConnectClient.getOrCreate(context);
Metadata metadata = Metadata.activelyRecorded(
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
);
ExerciseSessionRecord record = new ExerciseSessionRecord( List<Record> records = new ArrayList<>();
ExerciseData exerciseData = createExerciseData(contentProviderUtils, trackId);
//Avoid bug in HealthConnect: data must be within ExerciseSessionRecord.startTime and ExerciseSessionRecord.stopTime
List<ExerciseRoute.Location> locations = exerciseData.locations.stream()
.filter(i -> track.startTime().toInstant().isBefore(i.getTime()) && track.stopTime().toInstant().isAfter(i.getTime()))
.toList();
records.add(new ExerciseSessionRecord(
track.startTime().toInstant(), track.startTime().toInstant(),
track.startTime().getOffset(), track.startTime().getOffset(),
track.stopTime().toInstant(), track.stopTime().toInstant(),
track.stopTime().getOffset(), track.stopTime().getOffset(),
Metadata.activelyRecorded( metadata,
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
),
toExerciseType(track.activityType()), toExerciseType(track.activityType()),
track.name(), track.name(),
track.description(), track.description(),
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(),
exerciseRoute); new ExerciseRoute(locations)));
records.add(new DistanceRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().totalDistance().toM()),
metadata
));
if (track.statistics().altitudeGainLoss() != null) {
records.add(new ElevationGainedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().altitudeGainLoss().gain_m()),
metadata
));
}
if (!exerciseData.speeds.isEmpty()) {
records.add(new SpeedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
exerciseData.speeds,
metadata
));
}
List<ExerciseSessionRecord> records = Collections.singletonList(record);
Object ignored = BuildersKt.runBlocking( Object ignored = BuildersKt.runBlocking(
kotlin.coroutines.EmptyCoroutineContext.INSTANCE, kotlin.coroutines.EmptyCoroutineContext.INSTANCE,
(Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont)); (Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont));
@@ -140,30 +184,53 @@ public class HealthConnectWorker extends Worker {
return new ExportResult(track.name()); return new ExportResult(track.name());
} }
private static ExerciseRoute createExerciseRoute(ContentProviderUtils contentProviderUtils, Track.Id trackId) { private static ExerciseData createExerciseData(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
List<ExerciseRoute.Location> route = new ArrayList<>(); List<ExerciseRoute.Location> locations = new LinkedList<>();
List<SpeedRecord.Sample> speeds = new LinkedList<>();
try (TrackPointIterator iterator = contentProviderUtils.getTrackPointIterator(trackId, null)) { try (TrackPointIterator iterator = contentProviderUtils.getTrackPointIterator(trackId, null)) {
while (iterator.hasNext()) { while (iterator.hasNext()) { //TODO iterator.iterator.forEachRemaining()
TrackPoint trackPoint = iterator.next(); TrackPoint trackPoint = iterator.next();
if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue; if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue; //TODO Include SEGMENT_START_AUTOMATIC?
Position position = trackPoint.position(); Position position = trackPoint.position();
route.add(new ExerciseRoute.Location( if (position.hasLocation()) {
trackPoint.getTime(), //ExcerciseRoute does not allow duplicated timestamps.
position.latitude(), if (locations.stream().noneMatch(location -> location.getTime().equals(position.time()))) {
position.longitude(), locations.add(new ExerciseRoute.Location(
position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null, trackPoint.getTime(),
position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null, position.latitude(),
position.altitude() != null ? Length.meters(position.altitude().toM()) : null) position.longitude(),
); position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null,
position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null,
position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
);
}
}
if (position.hasSpeed()) {
speeds.add(new SpeedRecord.Sample(
trackPoint.getTime(),
Velocity.metersPerSecond(position.speed().toMPS())
));
}
//TODO Cadence
//TODO Power
//TODO Heartrate
} }
} }
return new ExerciseRoute(route); return new ExerciseData(locations, speeds);
} }
private record ExerciseData(
List<ExerciseRoute.Location> locations,
List<SpeedRecord.Sample> speeds
) {}
private static int toExerciseType(ActivityType activityType) { private static int toExerciseType(ActivityType activityType) {
return switch (activityType) { return switch (activityType) {
case RUNNING, STREET_RUNNING, TRAIL_RUNNING, TRACK_RUNNING -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING; case RUNNING, STREET_RUNNING, TRAIL_RUNNING, TRACK_RUNNING -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING;
@@ -45,8 +45,9 @@ public class GpsHandler extends SensorHandler<Location, Position> {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval()); gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
} }
//TODO if (driver != null) {
connect(); driver.updateDriverParameters(getContext(), sensorManager.getHandler());
}
} }
}; };
@@ -22,11 +22,11 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
private static final String TAG = SensorHandler.class.getSimpleName(); private static final String TAG = SensorHandler.class.getSimpleName();
private final SensorManager sensorManager;
@VisibleForTesting @VisibleForTesting
public Driver driver; public Driver driver;
protected final SensorManager sensorManager;
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator; protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
@Nullable @Nullable
@@ -11,5 +11,9 @@ public interface Driver {
//TODO Remove from interface? //TODO Remove from interface?
boolean isConnected(); boolean isConnected();
default void updateDriverParameters(Context context, Handler handler) {
throw new UnsupportedOperationException();
}
void disconnect(); void disconnect();
} }
@@ -57,16 +57,11 @@ public class GpsInternal implements Driver {
return; return;
} }
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) { if (PermissionRequester.GPS.hasPermission(context)) {
try { try {
Log.i(TAG, "Register for location updates " + context); Log.i(TAG, "Register for location updates");
driverObserver.onConnected(null, null); driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat); LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, createLocationRequest(), handler::post, locationListenerCompat);
return; return;
} catch (SecurityException e) { } catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e); Log.e(TAG, "Could not register location listener; permissions not granted.", e);
@@ -76,6 +71,16 @@ public class GpsInternal implements Driver {
driverObserver.onDisconnected(); driverObserver.onDisconnected();
} }
@SuppressWarnings({"MissingPermission"})
@Override
public void updateDriverParameters(Context context, Handler handler) {
if (!isConnected()) return;
Log.i(TAG, "Update for location updates");
driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, createLocationRequest(), handler::post, locationListenerCompat);
}
@SuppressWarnings({"MissingPermission"}) @SuppressWarnings({"MissingPermission"})
@Override @Override
public void disconnect() { public void disconnect() {
@@ -87,9 +92,15 @@ public class GpsInternal implements Driver {
driverObserver.onDisconnected(); driverObserver.onDisconnected();
} }
@Override @Override
public boolean isConnected() { public boolean isConnected() {
return locationManager != null; return locationManager != null;
} }
private LocationRequestCompat createLocationRequest() {
return new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
}
} }
@@ -172,6 +172,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
} }
synchronized void endCurrentTrack() { synchronized void endCurrentTrack() {
handler.removeCallbacks(ON_IDLE);
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd(); TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
insertTrackPoint(segmentEnd, true); insertTrackPoint(segmentEnd, true);
@@ -188,6 +189,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
} }
RecordingData getDataForUI() { RecordingData getDataForUI() {
if (trackId == null) {
throw new RuntimeException("Requesting data if not recording should not be done.");
}
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation); Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation);
TrackPoint trackPoint = current.first; TrackPoint trackPoint = current.first;
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, trackPoint); TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, trackPoint);
@@ -196,8 +201,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB
if (track == null) { if (track == null) {
Log.w(TAG, "Requesting data if not recording is taking place, should not be done."); throw new RuntimeException("Data could not be loaded for TrackId " + trackId);
return null;
} }
TrackBuilder trackBuilder = new TrackBuilder(track); TrackBuilder trackBuilder = new TrackBuilder(track);
@@ -27,11 +27,6 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
setExportTrackFileFormatOptions(); setExportTrackFileFormatOptions();
setExportDirectorySummary(); setExportDirectorySummary();
setFilenameTemplate(); setFilenameTemplate();
findPreference("healthConnectPermissions").setSummaryProvider(preference -> {
//TODO This should be localized and prefixes trimmed.
return String.join(",", HealthConnectUtils.getGrantedPermissions(getActivity()));
});
} }
@Override @Override
@@ -73,7 +73,6 @@ public class PreferencesUtils {
private static SharedPreferences sharedPreferences; private static SharedPreferences sharedPreferences;
private static Resources resources; private static Resources resources;
private static final long LONG_UNSET = Long.MIN_VALUE;
/** /**
* Must be called during application startup. * Must be called during application startup.
@@ -560,6 +559,7 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length]; String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default)); final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
final int maxRecordingDistanceOff = Integer.parseInt(resources.getString(R.string.max_recording_distance_off));
UnitSystem unitSystem = getUnitSystem(); UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder() DistanceFormatter formatter = DistanceFormatter.Builder()
@@ -576,6 +576,9 @@ public class PreferencesUtils {
case METRIC, IMPERIAL_METER -> { case METRIC, IMPERIAL_METER -> {
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value); entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
@@ -583,6 +586,9 @@ public class PreferencesUtils {
case IMPERIAL_FEET, NAUTICAL_IMPERIAL -> { case IMPERIAL_FEET, NAUTICAL_IMPERIAL -> {
if (value == maxRecordingDistanceDefault) { if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT()); entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else { } else {
entries[i] = displayValue; entries[i] = displayValue;
} }
+1 -1
View File
@@ -302,7 +302,7 @@ limitations under the License.
<string name="waypoint_type_water_fountain">συντριβάνι</string> <string name="waypoint_type_water_fountain">συντριβάνι</string>
<string name="waypoint_type_waypoint">σημείο διαδρομής</string> <string name="waypoint_type_waypoint">σημείο διαδρομής</string>
<string name="about_preference_title">Σχετικά</string> <string name="about_preference_title">Σχετικά</string>
<string name="about_url">Ιστότοπους εγχειρήματος: <a href="%1$s">%1$s</a></string> <string name="about_url">Ιστότοπος εγχειρήματος: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Όνομα έκδοσης: %1$s</string> <string name="about_version_name">Όνομα έκδοσης: %1$s</string>
<string name="about_version_code">Κωδικός έκδοσης: %1$d</string> <string name="about_version_code">Κωδικός έκδοσης: %1$d</string>
<string name="about_third_party_data">Δεδομένα από τρίτους</string> <string name="about_third_party_data">Δεδομένα από τρίτους</string>
+34 -21
View File
@@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--><resources> --><resources>
<string name="about_preference_title">Acerca de</string> <string name="about_preference_title">Acerca de</string>
<string name="about_description"><i> OpenTracks </i> es una aplicación de seguimiento deportivo que respeta completamente su privacidad.</string> <string name="about_description"><i>OpenTracks</i> ies una aplicación de seguimiento deportivo que respeta por completo tu privacidad.</string>
<string name="about_url">Página web del proyecto: <a href="%1$s">%1$s</a></string> <string name="about_url">Página web del proyecto: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Nombre de la versión: %1$s</string> <string name="about_version_name">Nombre de la versión: %1$s</string>
<string name="about_version_code">Código de versión: %1$d</string> <string name="about_version_code">Código de versión: %1$d</string>
<string name="about_history">OpenTracks es una bifurcación de <a href="https://code.google.com/archive/p/mytracks/"> <i> MyTracks de Google </i> </a> (descontinuado en 2016).</string> <string name="about_history">OpenTracks es una bifurcación de <a href="https://code.google.com/archive/p/mytracks/"><i>MyTracks de Google</i></a> (descontinuada en 2016).</string>
<string name="about_libraries">Bibliotecas</string> <string name="about_libraries">Bibliotecas</string>
<!-- Help --> <!-- Help -->
<string name="help_feature_title">OpenTracks tiene las siguientes características:</string> <string name="help_feature_title">OpenTracks tiene las siguientes características:</string>
@@ -30,10 +30,7 @@ limitations under the License.
\n* funciona completamente sin conexión \n* funciona completamente sin conexión
\n* mostrar pistas en un mapa (ver más abajo)</string> \n* mostrar pistas en un mapa (ver más abajo)</string>
<string name="help_track_map_title">¿Cómo mostrar un recorrido en un mapa\?</string> <string name="help_track_map_title">¿Cómo mostrar un recorrido en un mapa\?</string>
<string name="help_track_map_content"> Utilice el pequeño icono de mapa. <string name="help_track_map_content">Utiliza el icono del mapa pequeño.\n a) (actualizaciones en tiempo real) Instala <a href="https://codeberg.org/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Ofrece mapas en línea o sin conexión a través de <a href="https://www.openstreetmap.org">OpenStreetMap</a>.\n En realidad, OSMDashboard es una aplicación independiente que solo puede acceder (temporalmente) a los datos de las rutas que se van a mostrar.\n b) A través de una aplicación de mapas normal (por ejemplo, <a href="http://osmand.net/">OsmAnd</a> y <a href="https://maps.me/">MAPS.ME</a>). Estas aplicaciones deben ser compatibles con el formato de archivo KMZ (solo ubicaciones y marcas de tiempo). De hecho, una aplicación debe ser compatible con <i>ACTION_VIEW</i> con el MIME <i>application/vnd.google-earth.kmz</i>.</string>
\na) (actualizaciones en vivo) Instale <a href="https://codeberg.org/OpenTracksApp/OSMDashboard"> OSMDashboard </a>. Proporciona mapas en línea o fuera de línea a través de <a href="https://www.openstreetmap.org"> OpenStreetMap </a>.
\nEn realidad, OSMDashboard es una aplicación separada que solo puede acceder (temporalmente) a los datos de los recorridos que se mostrarán.
\nb) A través de una aplicación de mapas normal (p. ej., <a href="http://osmand.net/"> OsmAnd </a> y <a href="https://maps.me/"> MAPS.ME </a>). Estas aplicaciones deben admitir el formato de archivo KMZ (solo ubicaciones y marcas de tiempo). De hecho, una aplicación debe admitir <i> ACTION_VIEW </i> con MIME <i> application / vnd.google-earth.kmz</i>. </string>
<string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string> <string name="help_storage_pictures_title">¿Cómo almacena OpenTracks las imágenes que se tomaron como marcadores\?</string>
<string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string> <string name="help_storage_pictures_content">Estos se almacenan en el almacenamiento interno de la aplicación (es decir, solo se puede acceder a ellos dentro de OpenTracks).</string>
<string name="help_track_export_title">¿Cómo puedo exportar/compartir recorridos grabados\?</string> <string name="help_track_export_title">¿Cómo puedo exportar/compartir recorridos grabados\?</string>
@@ -44,22 +41,9 @@ limitations under the License.
<string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores?</string> <string name="help_data_storage_title">¿Cómo almacena OpenTracks los datos de los sensores?</string>
<string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de un sensor de frecuencia cardíaca, esto significa que solo se almacena <i>una</i> medición de frecuencia cardíaca por ubicación. Si está parado o está en interiores, no se registrarán los datos del sensor (pero se mostrarán). </string> <string name="help_data_storage_content"> Los datos del sensor se almacenan por ubicación registrada. En términos de un sensor de frecuencia cardíaca, esto significa que solo se almacena <i>una</i> medición de frecuencia cardíaca por ubicación. Si está parado o está en interiores, no se registrarán los datos del sensor (pero se mostrarán). </string>
<string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string> <string name="help_gps_troubleshooting_title">¿OpenTracks no registra ubicaciones\?</string>
<string name="help_gps_troubleshooting_content"> Lo que puedes hacer: <string name="help_gps_troubleshooting_content">¿Qué puedes hacer?:\n a) comprueba si el GPS está activado,\n b) mejora la recepción del GPS (por ejemplo, sal al aire libre),\n c) compruébalo con la aplicación <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, y\n d) desactiva el modo de ahorro de energía.\n \n Si nada de esto funciona, por favor, informa de un error.</string>
\na) compruebe si el GPS está habilitado,
\nb) mejorar la recepción del GPS (por ejemplo, salir al aire libre),
\nc) verifique con la aplicación <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/"> GPSTest </a>, y
\nd) desactivar el ahorro de energía.
\n
\nSi todo no ayuda, informe un error. </string>
<string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string> <string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string>
<string name="help_support_content"> Usted puede: <string name="help_support_content">Puedes:\n a) dar a conocer OpenTracks,\n b) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">proponer nuevas funciones</a>,\n c) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues">informar de errores</a>,\n d) colaborar en el desarrollo del software,\n e) comprarlo en <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,\n f) hacer una donación, y\n g) hacer lo que creas que ayuda a OpenTracks.</string>
\na) haga correr la voz sobre OpenTracks,
\nb) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues"> proponer funciones </a>,
\nc) <a href="https://codeberg.org/OpenTracksApp/OpenTracks/issues"> informar errores </a>,
\nd) ayuda en el desarrollo de software,
\ne) comprar en <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play Store </a>,
\nf) donar, y
\ng) haga lo que crea que ayuda a OpenTracks. </string>
<!-- Activity Type --> <!-- Activity Type -->
<string name="activity_type_airplane">avión</string> <string name="activity_type_airplane">avión</string>
<string name="activity_type_atv">vehículos todo terreno</string> <string name="activity_type_atv">vehículos todo terreno</string>
@@ -655,4 +639,33 @@ limitations under the License.
<string name="settings_locale_title">Idioma</string> <string name="settings_locale_title">Idioma</string>
<string name="settings_locale_system_default">Predeterminado del sistema</string> <string name="settings_locale_system_default">Predeterminado del sistema</string>
<string name="stats_gain_loss">Ganancia y pérdida</string> <string name="stats_gain_loss">Ganancia y pérdida</string>
<string name="health_connect_permissions_rationale_title">Permisos de Health Connect</string>
<string name="health_connect_permissions_rationale_heading">¿Cómo usa OpenTracks Health Connect?</string>
<string name="health_connect_permissions_rationale_summary">Si se activa en los ajustes, OpenTracks registra los entrenamientos en Health Connect. OpenTracks no lee datos de Health Connect.</string>
<string name="activity_type_gravel_biking">ciclismo de grava</string>
<string name="description_sensor_temperature">Temperatura (Celsius)</string>
<string name="marker_detail_title">%1$d de %2$d: %3$s</string>
<string name="menu_export_all_to_health_connect">Exportar todo</string>
<string name="sensor_state_temperature">Termómetro</string>
<string name="settings_charts_title">Gráficos</string>
<string name="settings_chart_display_elevation_title">Mostrar altitud</string>
<string name="settings_chart_display_pace_or_speed_title">Ver ritmo o velocidad</string>
<string name="settings_chart_display_heart_rate_title">Ver pulso</string>
<string name="settings_recording_show_gps_disabled_warning_title">GPS desactivado</string>
<string name="settings_recording_show_gps_disabled_warning_summary">Aviso de GPS desactivado al grabar.</string>
<string name="settings_night_mode_option_system_dynamic">Sistema (Color dinámico)</string>
<string name="settings_night_mode_option_day_dynamic">Día (Color dinámico)</string>
<string name="settings_night_mode_option_night_dynamic">Noche (Color dinámico)</string>
<string name="settings_night_mode_option_night_oled_dynamic">Optimizado para OLED (Color dinámico)</string>
<string name="settings_announcements_segment_average_speed_pace">Velocidad/ritmo medio (tramo)</string>
<string name="settings_export_to_health_connect">Exportar a Health Connect</string>
<string name="stats_average_segment_pace">Ritmo medio del tramo</string>
<string name="stats_elapsed_segment_time">Tiempo de tramo transcurrido</string>
<string name="stats_sensors_temperature">Temperatura</string>
<string name="track_edit_time_offset">Diferencia horaria</string>
<string name="lap_heart_rate">Pulso (vuelta)</string>
<string name="health_connect_export_enabled_summary">Exportar la actividad a Health Connect al finalizar</string>
<string name="health_connect_not_available">Dispositivo no compatible con Health Connect</string>
<string name="health_connect_open_settings_title">Ajustes de Health Connect</string>
<string name="health_connect_open_settings_summary">Abrir los ajustes de Health Connect</string>
</resources> </resources>
+7
View File
@@ -598,4 +598,11 @@ limitations under the License.
<string name="activity_type_gravel_biking">rattasõit kruusateedel</string> <string name="activity_type_gravel_biking">rattasõit kruusateedel</string>
<string name="menu_export_all_to_health_connect">Ekspordi kõik</string> <string name="menu_export_all_to_health_connect">Ekspordi kõik</string>
<string name="settings_export_to_health_connect">Ekspordi „Health Connect“ liidesesse</string> <string name="settings_export_to_health_connect">Ekspordi „Health Connect“ liidesesse</string>
<string name="health_connect_export_enabled_summary">Peale salvestamise lõppemist ekspordi tegevus „Health Connect“ taustateenusesse</string>
<string name="health_connect_not_available">„Health Connect“ taustateenus pole selles seadmes saadaval</string>
<string name="health_connect_open_settings_title">„Health Connect“ taustateenuse seadistused</string>
<string name="health_connect_open_settings_summary">Ava „Health Connect“ taustateenuse seadistused</string>
<string name="health_connect_permissions_rationale_title">„Health Connect“ taustateenuse õigused</string>
<string name="health_connect_permissions_rationale_heading">Kuidas OpenTracks kasutab „Health Connect“ taustateenust</string>
<string name="health_connect_permissions_rationale_summary">Kui see on seadistustest lubatud, siis OpenTracks salvestab treeningute andmed ka „Health Connect“ taustateenusesse. OpenTracks ei loe andmeid „Health Connect“ taustateenusest.</string>
</resources> </resources>
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
+47 -33
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--><resources> --><resources>
<string name="about_preference_title">關於</string> <string name="about_preference_title">關於</string>
<string name="about_description"><i>OpenTracks</i> 是一個完全尊重您的隱私的運動蹤跡應用程式。</string> <string name="about_description"><i>OpenTracks</i> 是一款完全尊重您隱私的運動追蹤應用程式。</string>
<string name="about_url">專案網頁:<a href="%1$s">%1$s</a></string> <string name="about_url">專案網頁:<a href="%1$s">%1$s</a></string>
<string name="about_version_name">版本名稱:%1$s</string> <string name="about_version_name">版本名稱:%1$s</string>
<string name="about_version_code">版本代號:%1$d</string> <string name="about_version_code">版本代號:%1$d</string>
@@ -31,14 +31,11 @@ limitations under the License.
\n * 完全離線工作 \n * 完全離線工作
\n * 在地圖上顯示蹤跡(見下文)</string> \n * 在地圖上顯示蹤跡(見下文)</string>
<string name="help_track_map_title">如何在地圖顯示一個蹤跡?</string> <string name="help_track_map_title">如何在地圖顯示一個蹤跡?</string>
<string name="help_track_map_content"> \u0020使用小的地圖圖示。 <string name="help_track_map_content">使用小型地圖圖示。\na)(即時更新)安裝 <a href="https://codeberg.org/OpenTracksApp/OSMDashboard">OSMDashboard</a>。它透過 <a href="https://www.openstreetmap.org">OpenStreetMap</a> 提供線上或離線地圖。\n實際上,OSMDashboard 是一個獨立的應用程式,只能(暫時)存取即將顯示的蹤跡資料。\nb) 透過一般地圖應用程式(例如 <a href="http://osmand.net/">OsmAnd</a> 和 <a href="https://maps.me/">MAPS.ME</a>)。這些應用程式必須支援 KMZ 檔案格式(僅包含位置與時間戳記)。實際上,應用程式需要支援 MIME 類型為 <i>application/vnd.google-earth.kmz</i> 的 <i>ACTION_VIEW</i>。</string>
\n \u0020a) (即時更新)安裝 <a href="https://codeberg.org/OpenTracksApp/OSMDashboard">OSMDashboard</a>。它透過 <a href="https://www.openstreetmap.org">OpenStreetMap</a> 提供線上或離線地圖。
\n \u0020實際上,OSMDashboard 是一個單獨的應用程式,只能(暫時)存取將要顯示的蹤跡資料。
\n \u0020b) \u0020透過一個普通的地圖應用程式(例如,<a href="http://osmand.net/">OsmAnd</a> 與 <a href="https://maps.me/">MAPS.ME</a>)。 這些 APP 需要支援 KMZ 檔案格式(只有位置與時間戳記)。 更準確地說,APP 需要支援 <i>ACTION_VIEW</i> 與 MIME <i>application/vnd.google-earth.kmz</i>。 \u0020</string>
<string name="help_storage_pictures_title">OpenTracks 如何存儲作為標記拍攝的照片?</string> <string name="help_storage_pictures_title">OpenTracks 如何存儲作為標記拍攝的照片?</string>
<string name="help_storage_pictures_content">這些儲存在應用程序內部儲存區中 (即,只能在 OpenTracks 中存取)。</string> <string name="help_storage_pictures_content">這些儲存在應用程序內部儲存區中 (即,僅限 OpenTracks 中存取)。</string>
<string name="help_track_export_title">如何匯出/分享 紀錄的蹤跡?</string> <string name="help_track_export_title">如何匯出/分享 紀錄的蹤跡?</string>
<string name="help_track_export_content"> 可以從設定匯出蹤跡為 KMZ、KML 或 GPX。KMZ/KML 全部資料;GPX 只有位置。通過<i>分享圖示</i>以分享蹤跡為 KMZ。 </string> <string name="help_track_export_content">可從「設定」中將蹤跡匯出為 KMZ、KML 或 GPX。KMZ/KML 包含所有資料;GPX 僅包含位置資訊。透過 <i>分享圖示</i> 分享的蹤跡會以 KMZ 格式分享。</string>
<string name="help_data_google_play_services_title">OpenTracks 是否需要/使用 Google Play 服務?</string> <string name="help_data_google_play_services_title">OpenTracks 是否需要/使用 Google Play 服務?</string>
<string name="help_data_google_play_services_content">否</string> <string name="help_data_google_play_services_content">否</string>
<string name="help_recording_title">OpenTracks 是如何記錄資料的?</string> <string name="help_recording_title">OpenTracks 是如何記錄資料的?</string>
@@ -47,15 +44,9 @@ limitations under the License.
如果 GPS 裝置回報的資料不准確 (例如,位置、速度、海拔),OpenTracks 對此無能為力。 如果 GPS 裝置回報的資料不准確 (例如,位置、速度、海拔),OpenTracks 對此無能為力。
</string> </string>
<string name="help_data_storage_title">OpenTracks 如何儲存感測器資料?</string> <string name="help_data_storage_title">OpenTracks 如何儲存感測器資料?</string>
<string name="help_data_storage_content"> 感測器資料儲存在每個記錄的位置。 就心率感測器而言,這意味著只有<i>一個</i>心率測量值被依照位置儲存。 如果您是站著不動或在室內,沒有感測器資料會被記錄(但它會被顯示)。 </string> <string name="help_data_storage_content">感測器資料會儲存在每個記錄的位置中。以心率感測器為例,這表示每個位置只會儲存 <i>一筆</i> 心率測量值。如果您站著不動或位於室內,將不會記錄感測器資料(但仍會顯示)。</string>
<string name="help_gps_troubleshooting_title">OpenTracks 沒有紀錄位置?</string> <string name="help_gps_troubleshooting_title">OpenTracks 沒有記錄位置?</string>
<string name="help_gps_troubleshooting_content"> \u0020您能做什麼: <string name="help_gps_troubleshooting_content">您可以進行以下操作:\na) 確認 GPS 是否已啟用;\nb) 改善 GPS 訊號接收(例如到戶外);\nc) 使用 <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app 進行檢查;以及\nd) 停用省電模式。\n\n如果上述方法都無法解決問題,請回報錯誤。</string>
\n 一)檢查 GPS 是否啟用,
\n 二)改善 GPS 收訊(例如,前往戶外),
\n 三)使用 <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app 檢查,以及
\n 四)停用省電。
\n
\n如果一切都沒有幫助,請回報 bug。 \u0020</string>
<string name="help_support_title">我如何支持 OpenTracks?</string> <string name="help_support_title">我如何支持 OpenTracks?</string>
<string name="help_support_content"> <string name="help_support_content">
您可以:\n 您可以:\n
@@ -213,13 +204,13 @@ limitations under the License.
<string name="generic_yesterday">昨日</string> <string name="generic_yesterday">昨日</string>
<string name="generic_dismiss">關閉</string> <string name="generic_dismiss">關閉</string>
<!-- Gps --> <!-- Gps -->
<string name="gps_starting">正在啟用 GPS</string> <string name="gps_starting">正在啟動 GPS</string>
<string name="gps_wait_for_better_signal">正在等待更清晰的 GPS 訊號</string> <string name="gps_wait_for_better_signal">正在等待更清晰的 GPS 訊號</string>
<string name="gps_wait_for_signal">正在等待 GPS 訊號</string> <string name="gps_wait_for_signal">正在等待 GPS 訊號</string>
<string name="gps_disabled_msg">GPS 禁用</string> <string name="gps_disabled_msg">GPS 禁用</string>
<string name="gps_fixed_and_ready">GPS 已修復並準備就緒</string> <string name="gps_fixed_and_ready">GPS 已修復並準備就緒</string>
<string name="gps_recording_status">%1$s:%2$s</string> <string name="gps_recording_status">%1$s:%2$s</string>
<string name="gps_recording_without_signal">OpenTracks 紀錄中,不包含 GPS 資料。</string> <string name="gps_recording_without_signal">OpenTracks 正在進行無 GPS 資料記錄。</string>
<!-- Image --> <!-- Image -->
<string name="image_marker_count">標記數</string> <string name="image_marker_count">標記數</string>
<string name="image_photo_marker">相片標記</string> <string name="image_photo_marker">相片標記</string>
@@ -326,7 +317,7 @@ limitations under the License.
<string name="settings_recording_fullscreen_on_while_recording_summary">當記錄時,在全螢幕中呈現。</string> <string name="settings_recording_fullscreen_on_while_recording_summary">當記錄時,在全螢幕中呈現。</string>
<string name="settings_recording_default_activity_title">預設活動類型</string> <string name="settings_recording_default_activity_title">預設活動類型</string>
<string name="settings_recording_location_frequency_summary">間隔 %1$s 檢查位置</string> <string name="settings_recording_location_frequency_summary">間隔 %1$s 檢查位置</string>
<string name="settings_recording_max_recording_distance_summary">系統會在兩個地點之間的距離大於 %1$s 時建立新的區段</string> <string name="settings_recording_max_recording_distance_summary">如果兩個地點之間的距離大於 %1$s,則建立一個新路段</string>
<string name="settings_recording_max_recording_distance_title">最遠距離</string> <string name="settings_recording_max_recording_distance_title">最遠距離</string>
<string name="settings_recording_min_recording_distance_title">記錄位置的距離間隔</string> <string name="settings_recording_min_recording_distance_title">記錄位置的距離間隔</string>
<string name="settings_recording_min_recording_interval_title">記錄位置的時間間隔</string> <string name="settings_recording_min_recording_interval_title">記錄位置的時間間隔</string>
@@ -337,8 +328,8 @@ limitations under the License.
<string name="settings_recording_track_name_title">預設蹤跡名稱</string> <string name="settings_recording_track_name_title">預設蹤跡名稱</string>
<string name="settings_night_mode_title">界面主題</string> <string name="settings_night_mode_title">界面主題</string>
<string name="settings_night_mode_option_system">系統</string> <string name="settings_night_mode_option_system">系統</string>
<string name="settings_night_mode_option_day">日</string> <string name="settings_night_mode_option_day">早</string>
<string name="settings_night_mode_option_night">夜</string> <string name="settings_night_mode_option_night">晚</string>
<string name="settings_announcements_totaltime_frequency">時間間隔</string> <string name="settings_announcements_totaltime_frequency">時間間隔</string>
<string name="settings_announcements_distance_frequency">單圈長度</string> <string name="settings_announcements_distance_frequency">單圈長度</string>
<string name="settings_announcements_average_heart_rate">平均心率</string> <string name="settings_announcements_average_heart_rate">平均心率</string>
@@ -383,12 +374,12 @@ limitations under the License.
<string name="settings_api_summary">其他 APP 的遠端控制與統計資料 API</string> <string name="settings_api_summary">其他 APP 的遠端控制與統計資料 API</string>
<string name="settings_public_api_package_title">公開 API 的套件名稱</string> <string name="settings_public_api_package_title">公開 API 的套件名稱</string>
<string name="settings_public_api_enabled_title">公開 API</string> <string name="settings_public_api_enabled_title">公開 API</string>
<string name="settings_public_api_enabled_summary_on">其他已安裝的應用程式可以開始或停止紀錄。</string> <string name="settings_public_api_enabled_summary_on">其他已安裝的應用程式可以啟動或停止記錄。</string>
<string name="settings_public_api_enabled_summary_off">只有 OpenTracks 可以開始或停止紀錄。</string> <string name="settings_public_api_enabled_summary_off">僅 OpenTracks 可以啟動和停止記錄。</string>
<string name="settings_public_api_disabled_toast">OpenTracks 公開 AIP 被禁用:它可以在設定中被啟用。</string> <string name="settings_public_api_disabled_toast">OpenTracks 公開 AIP 被禁用:它可以在設定中被啟用。</string>
<string name="settings_public_api_dashboard_enabled_title">自動資料傳輸</string> <string name="settings_public_api_dashboard_enabled_title">自動資料傳輸</string>
<string name="settings_public_api_dashboard_enabled_summary_on">開始紀錄的應用程式也可以存取紀錄的資料。</string> <string name="settings_public_api_dashboard_enabled_summary_on">已啟動記錄的應用程式也可以存取已記錄的資料。</string>
<string name="settings_public_api_dashboard_enabled_summary_off">記錄的資料將不會自動與其他應用程式共用。</string> <string name="settings_public_api_dashboard_enabled_summary_off">已記錄的資料不會自動與其他應用程式共用。</string>
<!-- Settings import/export --> <!-- Settings import/export -->
<string name="settings_import_section">匯入</string> <string name="settings_import_section">匯入</string>
<string name="settings_export_as_file_section">匯出</string> <string name="settings_export_as_file_section">匯出</string>
@@ -546,26 +537,26 @@ limitations under the License.
<string name="share_image_subject">我想與你分享一張圖片</string> <string name="share_image_subject">我想與你分享一張圖片</string>
<string name="share_image_body">我認為你可能會對這張圖片感興趣。</string> <string name="share_image_body">我認為你可能會對這張圖片感興趣。</string>
<string name="hold_to_stop">按住以停止</string> <string name="hold_to_stop">按住以停止</string>
<string name="instant_export_enabled_summary">在紀錄完成後匯出蹤跡至儲存區</string> <string name="instant_export_enabled_summary">在記錄完成後將蹤跡匯出至儲存區</string>
<string name="instant_export_enabled_title">即時運動後匯出</string> <string name="instant_export_enabled_title">即時運動後匯出</string>
<string name="settings_default_export_uri_title">蹤跡匯出目錄</string> <string name="settings_default_export_uri_title">蹤跡匯出目錄</string>
<!-- Fields --> <!-- Fields -->
<string name="field_set_primary">主要的</string> <string name="field_set_primary">主要的</string>
<string name="field_set_secondary">次要的</string> <string name="field_set_secondary">次要的</string>
<string name="field_remove_from_layout">移除</string> <string name="field_remove_from_layout">移除</string>
<string name="no_compatible_file_manager_installed">沒有安裝相容的檔案管理器</string> <string name="no_compatible_file_manager_installed">未安裝相容的檔案管理器</string>
<string name="error_crash_title">%1$s 已崩潰</string> <string name="error_crash_title">%1$s 已崩潰</string>
<string name="share_crash_report">分享</string> <string name="share_crash_report">分享</string>
<string name="report_bug">回報 bug</string> <string name="report_bug">回報 bug</string>
<string name="show_on_dashboard">OpenTracks 儀表板 API</string> <string name="show_on_dashboard">OpenTracks 儀表板 API</string>
<string name="show_on_dashboard_not_installed">沒有安裝支援 OpenTracks 儀表板 API 的應用程式。</string> <string name="show_on_dashboard_not_installed">未安裝支援 OpenTracks 儀表板 API 的應用程式。</string>
<string name="introduction_osm_dashboard">OpenTracks 本身不提供地圖。請安裝 OSMDashboard 以在地圖上檢視您的紀錄。</string> <string name="introduction_osm_dashboard">OpenTracks 本身不提供地圖功能。請安裝 OSMDashboard,以便在地圖上查看您的記錄。</string>
<string name="voice_on_device_speaker_title">使用裝置的揚聲器</string> <string name="voice_on_device_speaker_title">使用裝置的揚聲器</string>
<string name="settings_stats_units_imperial_meter">英制(英里、公尺)</string> <string name="settings_stats_units_imperial_meter">英制(英里、公尺)</string>
<string name="menu_export_all_one_file">全部匯出 (所有蹤跡於單一檔案)</string> <string name="menu_export_all_one_file">全部匯出 (所有蹤跡於單一檔案)</string>
<string name="settings_recording_min_required_accuracy_summary">如果定位的精度大於 %1$s,則它將被忽略</string> <string name="settings_recording_min_required_accuracy_summary">如果定位的精度大於 %1$s,則它將被忽略</string>
<string name="no_compatible_gallery_installed">沒有安裝相容的圖片庫應用程式</string> <string name="no_compatible_gallery_installed">未安裝相容的圖片相簿應用程式</string>
<string name="no_compatible_camera_installed">沒有安裝相容的攝影機應用程式</string> <string name="no_compatible_camera_installed">未安裝相容的相機應用程式</string>
<string name="settings_sensor_bluetooth_service_filter_title">篩選藍芽裝置</string> <string name="settings_sensor_bluetooth_service_filter_title">篩選藍芽裝置</string>
<string name="settings_sensor_bluetooth_service_filter_off">顯示所有可發現的藍芽裝置</string> <string name="settings_sensor_bluetooth_service_filter_off">顯示所有可發現的藍芽裝置</string>
<string name="voiceDistanceNauticalMilesPlural">{ n, plural, other {{n,number,#.0} 海里} }</string> <string name="voiceDistanceNauticalMilesPlural">{ n, plural, other {{n,number,#.0} 海里} }</string>
@@ -608,12 +599,35 @@ limitations under the License.
<string name="settings_announcements_lap_power">單圈功率</string> <string name="settings_announcements_lap_power">單圈功率</string>
<string name="create_marker_error">無法建立標記。請再次嘗試。</string> <string name="create_marker_error">無法建立標記。請再次嘗試。</string>
<string name="settings_theme_switch_restart">主題變更也許需要手動重新啟動。</string> <string name="settings_theme_switch_restart">主題變更也許需要手動重新啟動。</string>
<string name="settings_night_mode_option_night_oled">夜(OLED 友善的)</string> <string name="settings_night_mode_option_night_oled">暗色 OLED 友善</string>
<string name="power_x_watt">功率 {x, plural, =1 {1 瓦特} other {# 瓦特} }</string> <string name="power_x_watt">功率 {x, plural, =1 {1 瓦特} other {# 瓦特} }</string>
<string name="settings_charts_title">圖表</string> <string name="settings_charts_title">圖表</string>
<string name="stats_gain_loss">收益&損失</string> <string name="stats_gain_loss">收益&損失</string>
<string name="settings_recording_show_gps_disabled_warning_title">GPS 禁用警告</string> <string name="settings_recording_show_gps_disabled_warning_title">GPS 禁用警告</string>
<string name="settings_recording_show_gps_disabled_warning_summary">記錄時,如果 GPS 被禁用則顯示警告。</string> <string name="settings_recording_show_gps_disabled_warning_summary">當記錄時,如果 GPS 被禁用則顯示警告。</string>
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string> <string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string> <string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
<string name="activity_type_gravel_biking">碎石路騎行</string>
<string name="description_sensor_temperature">溫度(攝氏度)</string>
<string name="menu_export_all_to_health_connect">匯出全部</string>
<string name="sensor_state_temperature">體溫計</string>
<string name="settings_night_mode_option_system_dynamic">系統(動態色彩)</string>
<string name="settings_night_mode_option_day_dynamic">早(動態色彩)</string>
<string name="settings_export_to_health_connect">匯出至 Health Connect</string>
<string name="settings_night_mode_option_night_dynamic">晚(動態色彩)</string>
<string name="settings_night_mode_option_night_oled_dynamic">暗色 OLED 友善(動態色彩)</string>
<string name="settings_announcements_segment_average_speed_pace">平均速度/配速(路段)</string>
<string name="stats_average_segment_pace">平均路段配速</string>
<string name="stats_elapsed_segment_time">路段經過時間</string>
<string name="stats_coordinates">緯度/經度</string>
<string name="stats_sensors_temperature">溫度</string>
<string name="track_edit_time_offset">時區偏移值</string>
<string name="health_connect_export_enabled_summary">在記錄完成後將活動匯出至 Health Connect</string>
<string name="health_connect_not_available">此設備不支援 Health Connect 功能</string>
<string name="health_connect_open_settings_title">Health Connect 設定</string>
<string name="health_connect_open_settings_summary">開啟 Health Connect 設定</string>
<string name="health_connect_permissions_rationale_title">Health Connect 權限</string>
<string name="health_connect_permissions_rationale_heading">OpenTracks 如何使用 Health Connect</string>
<string name="health_connect_permissions_rationale_summary">如果在設定中啟用此功能,OpenTracks 會將運動資料寫入 Health Connect。 OpenTracks 不會從 Health Connect 讀取資料。</string>
<string name="marker_detail_title">%2$d 個中的 %1$d:%3$s</string>
</resources> </resources>
+2
View File
@@ -81,7 +81,9 @@
<string name="max_recording_distance_key" translatable="false">maxRecordingDistance</string> <string name="max_recording_distance_key" translatable="false">maxRecordingDistance</string>
<string name="max_recording_distance_default" translatable="false">200</string> <string name="max_recording_distance_default" translatable="false">200</string>
<string name="max_recording_distance_off" translatable="false">2147483647</string><!-- MAX_INT -->
<string-array name="max_recording_distance_values"> <string-array name="max_recording_distance_values">
<item>@string/max_recording_distance_off</item>
<item>50</item> <item>50</item>
<item>100</item> <item>100</item>
<item>@string/max_recording_distance_default</item> <item>@string/max_recording_distance_default</item>
@@ -65,14 +65,6 @@
android:targetPackage="@string/applicationId" /> android:targetPackage="@string/applicationId" />
</Preference> </Preference>
<Preference
android:title="@string/health_connect_permissions_rationale_title"
android:key="healthConnectPermissions">
<intent
android:targetClass="de.dennisguse.opentracks.settings.HealthConnect$ConfigurePermissions"
android:targetPackage="@string/applicationId" />
</Preference>
<Preference android:title="@string/menu_export_all_to_health_connect"> <Preference android:title="@string/menu_export_all_to_health_connect">
<intent <intent
android:targetClass="de.dennisguse.opentracks.settings.HealthConnect$ExportAll" android:targetClass="de.dennisguse.opentracks.settings.HealthConnect$ExportAll"