mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-01 01:02:21 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a63298ef6e | |||
| 1294e944a7 | |||
| 7258335180 | |||
| 139d72affe | |||
| 3791ccf888 | |||
| dc78615c83 | |||
| 53dfa915fb | |||
| 7c6141f98c | |||
| f8279c7095 | |||
| 465e3e9be8 | |||
| f38999a970 | |||
| 4dfd2485bf | |||
| 10273cd772 | |||
| fbfe6f3ab8 | |||
| 86726c5e0e | |||
| b711c4fdc5 | |||
| cefdb1280a |
@@ -0,0 +1,31 @@
|
||||
name: Build and Release APK
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Upload APK
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build APKs
|
||||
shell: bash
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Upload to Gitea Releases
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: latest
|
||||
name: Latest Build (main)
|
||||
prerelease: true
|
||||
files: build/outputs/apk/**/*.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,12 +8,12 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
name: Android Lint
|
||||
runs-on: docker
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
run: ./gradlew lint
|
||||
|
||||
android-check:
|
||||
runs-on: docker
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
image: 'cimg/android:2024.04-node'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="OpenTracks">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="androidTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="unitTest">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="main">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -45,6 +45,18 @@ Translations are hosted on <a href="https://translate.codeberg.org/engage/opentr
|
||||
<img src="https://translate.codeberg.org/widget/opentracks/opentracks-src-main-res-values-strings-xml-main/horizontal-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## ⚠️ About This Fork
|
||||
|
||||
This repository is a customized fork of the official OpenTracks application.
|
||||
|
||||
**Core Purpose:**
|
||||
The primary goal of this fork is to fundamentally change how Bluetooth sensors are prioritized over GPS data. In this modified version, Bluetooth sensor data is given absolute priority and actively ignores/overrides GPS signals in most scenarios. This ensures that cadence, speed, and distance measurements strictly rely on hardware sensors rather than falling back to or being averaged with GPS data.
|
||||
|
||||
**Consequential Changes:**
|
||||
As a direct result of these sensor priority changes, several downstream modifications were implemented to support the new data flow:
|
||||
* **UI & Settings:** Expanded settings menus and customized recording layouts to better expose the raw sensor data and give the user finer control.
|
||||
* **Media & Voice Announcements:** Integrated media playback controls directly into the statistics view and overhauled voice announcements to handle audio focus and integrate with the new sensor-first approach.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:9.3.2'
|
||||
classpath 'com.android.tools.build:gradle:9.4.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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:
|
||||
- Cambio de nombre de MyTracks a OpenTracks
|
||||
- Se agregó un sensor de frecuencia cardíaca Bluetooth LE
|
||||
- Se eliminaron todas las funciones que requieren acceso a Internet.
|
||||
- Se eliminó la integración de Google Maps
|
||||
- Se eliminó el uso de los servicios de Google Play.
|
||||
- Se eliminaron los sensores ANT+
|
||||
- Se eliminaron los sensores clásicos de Bluetooth.
|
||||
- Se eliminó Protobuf
|
||||
- Se eliminaron 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 añadido el sensor de frecuencia cardíaca Bluetooth LE
|
||||
- Se han eliminado todas las funciones que requerían acceso a Internet
|
||||
- Se ha eliminado la integración con Google Maps
|
||||
- Se ha eliminado el uso de los servicios de Google Play
|
||||
- Se han eliminado los sensores ANT+
|
||||
- Se han eliminado los sensores Bluetooth clásicos
|
||||
- Se ha eliminado Protobuf
|
||||
- Se han eliminado la estimación de calorías y la estimación de actividad
|
||||
- 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
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
v3.1.0: OpenTracks
|
||||
|
||||
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 posibilidad de visualizar las estadísticas de seguimiento sin desbloquear el dispositivo durante el mismo.
|
||||
- Se ha eliminado el modo horizontal de la vista de detalles de la pista (debido a un error).
|
||||
- El historial de sugerencias de búsqueda ha sido eliminado.
|
||||
- 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 opción de mostrar las estadísticas del recorrido sin desbloquear el dispositivo mientras se realiza el seguimiento
|
||||
- Se ha eliminado el modo horizontal para la vista detallada del recorrido (presentaba errores)
|
||||
- Se ha eliminado el historial de sugerencias de búsqueda
|
||||
|
||||
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:
|
||||
- Actualizar a Gradle 5.6
|
||||
- No minimice la versión de lanzamiento
|
||||
Desarrollador:
|
||||
- Actualización a Gradle 5.6
|
||||
- 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
|
||||
|
||||
Cambios:
|
||||
- Detección de inactividad se puede desactivar
|
||||
- Se puede desactivar la detección de inactividad
|
||||
|
||||
Correcciones de errores:
|
||||
- UI para añadir imágenes a los marcadores estaba oculto
|
||||
Corrección de errores:
|
||||
- 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:
|
||||
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
|
||||
* Marca lugares de interés, incluso agrega fotos a los marcadores
|
||||
* Pulsómetro (compatible con dispositivos Bluetooth LE)
|
||||
* Exporta tus rutas como KML o GPX
|
||||
* Completamente offline, no necesitas internet
|
||||
El deporte y las actividades al aire libre aportan felicidad, destreza y confianza.
|
||||
|
||||
Cuida tu salud llevando un registro de tu entrenamiento.
|
||||
Registra tus sesiones de carrera o caminata, y te ofrece un ciclocomputador con una pantalla más grande para el ciclismo.
|
||||
Marca lugares interesantes a lo largo de tu recorrido con fotos.
|
||||
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.
|
||||
|
||||
@@ -11,12 +11,12 @@ Hanya membagikan data yang Anda ingin orang lain memilikinya.
|
||||
* Notifikasi suara.
|
||||
* Mendukung sensor Bluetooth LE: detak jantung, kecepatan dan jarak (bersepeda), irama (bersepeda), dan pengukur daya (bersepeda).
|
||||
* 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.
|
||||
* Tidak ada akses Internet atau izin tambahan.
|
||||
* Tema gelap dan terang, dengan memperhatikan pengaturan sistem.
|
||||
* 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.
|
||||
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 @@
|
||||
一個尊重您的隱私的運動蹤跡紀錄器。
|
||||
一款尊重您隱私的運動蹤跡紀錄器。
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#This file is generated by updateDaemonJvm
|
||||
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||
toolchainVendor=JETBRAINS
|
||||
toolchainVersion=21
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
cat << 'EOC' > src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||
--- src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||
+++ src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||
@@ -351,6 +351,11 @@
|
||||
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||
return getBoolean(R.string.settings_announcements_long_break_key, true);
|
||||
}
|
||||
+
|
||||
+ public static long getLongBreakDuration() {
|
||||
+ int minutes = getInt(R.string.settings_long_break_duration_key, 3);
|
||||
+ return minutes * 60 * 1000L;
|
||||
+ }
|
||||
EOC
|
||||
patch -p0 < src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||
@@ -0,0 +1,13 @@
|
||||
cat << 'EOC' > src/main/res/values/strings.xml.patch
|
||||
--- src/main/res/values/strings.xml
|
||||
+++ src/main/res/values/strings.xml
|
||||
@@ -1071,6 +1071,8 @@
|
||||
|
||||
<string name="settings_announcements_long_break_key">settings_announcements_long_break_key</string>
|
||||
<string name="settings_announcements_long_break_title">Long break</string>
|
||||
+ <string name="settings_long_break_duration_key">settings_long_break_duration_key</string>
|
||||
+ <string name="settings_long_break_duration_title">Long break duration (minutes)</string>
|
||||
|
||||
</resources>
|
||||
EOC
|
||||
patch -p0 < src/main/res/values/strings.xml.patch
|
||||
@@ -0,0 +1 @@
|
||||
cat src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java | grep -n "if (movingDistance != null) {" -B 2 -A 30
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
rootProject.name = 'OpenTracks'
|
||||
+68
-17
@@ -92,7 +92,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -132,7 +135,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -172,7 +178,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -212,7 +221,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -254,7 +266,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -276,7 +291,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -316,7 +334,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -338,7 +359,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -378,7 +402,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -418,7 +445,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -458,7 +488,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -498,7 +531,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -520,7 +556,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -560,7 +599,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -582,7 +624,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -610,7 +655,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -644,7 +692,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
|
||||
@@ -424,6 +424,16 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
android:label="@string/recording_service" />
|
||||
|
||||
<service
|
||||
android:name=".services.MediaNotificationListenerService"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -67,6 +67,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
|
||||
private Snackbar snackbar;
|
||||
private Snackbar sensorWarningSnackbar;
|
||||
|
||||
// The following are setFrequency in onCreate
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -86,6 +87,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
service.getGpsStatusObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
||||
|
||||
service.getSensorDisconnectedWarningObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onSensorDisconnectedWarningChanged);
|
||||
|
||||
if (!service.isRecording()) {
|
||||
finish();
|
||||
return;
|
||||
@@ -408,4 +412,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
private void onSensorDisconnectedWarningChanged(boolean show) {
|
||||
if (show) {
|
||||
if (sensorWarningSnackbar == null) {
|
||||
sensorWarningSnackbar = Snackbar
|
||||
.make(viewBinding.trackRecordingCoordinatorLayout,
|
||||
getString(R.string.sensor_disconnected_warning),
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(getString(R.string.sensor_disconnected_switch_action), v -> {
|
||||
if (trackRecordingServiceConnection != null && trackRecordingServiceConnection.getTrackRecordingService() != null) {
|
||||
trackRecordingServiceConnection.getTrackRecordingService().setOverrideSensorWithGPS(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.show();
|
||||
}
|
||||
} else {
|
||||
if (sensorWarningSnackbar != null && sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +151,14 @@ public class SegmentStatisticUpdater {
|
||||
}
|
||||
|
||||
public void addMovingTime(TrackPoint trackPoint, TrackPoint lastTrackPoint) {
|
||||
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
addMovingTime(Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()));
|
||||
}
|
||||
|
||||
if (movingDuration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
|
||||
public void addMovingTime(Duration duration) {
|
||||
if (duration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative");
|
||||
}
|
||||
this.movingDuration = this.movingDuration.plus(movingDuration);
|
||||
movingDuration = movingDuration.plus(duration);
|
||||
}
|
||||
|
||||
private Speed getAverageMovingSpeed() {
|
||||
|
||||
+21
-11
@@ -143,7 +143,6 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
{
|
||||
// Update total distance
|
||||
Distance movingDistance = null;
|
||||
if (trackPoint.sensorDistance() != null) {
|
||||
movingDistance = trackPoint.sensorDistance();
|
||||
@@ -153,16 +152,7 @@ public class TrackStatisticsUpdater {
|
||||
// GPS-based distance/speed
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
if (movingDistance != null) {
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!isIdle
|
||||
&& !trackPoint.isSegmentManualStart()
|
||||
&& lastTrackPoint != null) {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
|
||||
boolean isIdleTransition = false;
|
||||
if (trackPoint.isIdleTriggered()) {
|
||||
isIdle = true;
|
||||
} else if (isIdle) {
|
||||
@@ -170,6 +160,26 @@ public class TrackStatisticsUpdater {
|
||||
if (movingDistance != null
|
||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||
isIdle = false;
|
||||
isIdleTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (movingDistance != null && !isIdle) {
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!isIdle
|
||||
&& !trackPoint.isSegmentManualStart()
|
||||
&& lastTrackPoint != null) {
|
||||
if (isIdleTransition) {
|
||||
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0) {
|
||||
double timeSeconds = movingDistance.toM() / trackPoint.position().speed().toMPS();
|
||||
currentSegment.addMovingTime(Duration.ofMillis((long)(timeSeconds * 1000)));
|
||||
} else {
|
||||
currentSegment.addMovingTime(Duration.ofMillis((long)(movingDistance.toM() * 1000)));
|
||||
}
|
||||
} else {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ public class ExportActivity extends AbstractActivity {
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
// 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 Handler conflictsHandler = new Handler();
|
||||
@@ -183,7 +183,7 @@ public class ExportActivity extends AbstractActivity {
|
||||
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
|
||||
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
|
||||
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
|
||||
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
|
||||
exportTasks.addAll(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
|
||||
setProgress();
|
||||
nextExport(null);
|
||||
}
|
||||
@@ -226,7 +226,6 @@ public class ExportActivity extends AbstractActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
exportTasks = new ArrayList<>();
|
||||
if (allInOneFile) {
|
||||
String filename = "OpenTracks-Backup";
|
||||
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::id).toList()));
|
||||
|
||||
@@ -189,6 +189,17 @@ public class DataProvider {
|
||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
}
|
||||
|
||||
// Explicitly grant URI permissions to all packages that can resolve this intent.
|
||||
// This works around issues where ClipData multiple URI grants fail on some Android versions.
|
||||
android.content.pm.PackageManager packageManager = context.getPackageManager();
|
||||
java.util.List<android.content.pm.ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY);
|
||||
for (android.content.pm.ResolveInfo resolveInfo : resolveInfos) {
|
||||
String packageName = resolveInfo.activityInfo.packageName;
|
||||
for (Uri uri : uris) {
|
||||
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||
@@ -28,6 +29,9 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
|
||||
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
||||
|
||||
@Nullable
|
||||
private Integer batteryLevel = null;
|
||||
|
||||
protected SensorHandler(SensorManager sensorManager) {
|
||||
this.sensorManager = sensorManager;
|
||||
}
|
||||
@@ -115,7 +119,8 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
return;
|
||||
}
|
||||
|
||||
SensorHandler.this.aggregator.resetAggregated();
|
||||
SensorHandler.this.aggregator = null;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,5 +135,18 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
SensorHandler.this.aggregator = null;
|
||||
batteryLevel = null;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatteryLevelReceived(int percentage) {
|
||||
batteryLevel = percentage;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getBatteryLevel() {
|
||||
return batteryLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
@@ -239,6 +241,9 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
if (!isStarted()) {
|
||||
return new SensorDataSet(new SensorData<>(Position.of(now), ""), null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
//We always need a Position with now
|
||||
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
||||
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
||||
@@ -247,6 +252,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
gpsSensorData = new SensorData<>(Position.of(now), "");
|
||||
}
|
||||
|
||||
BatteryLevel cadenceBattery = cyclingCadenceHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(cyclingCadenceHandler.getBatteryLevel()) : null;
|
||||
BatteryLevel speedBattery = cyclingDistanceSpeedHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(cyclingDistanceSpeedHandler.getBatteryLevel()) : null;
|
||||
BatteryLevel heartRateBattery = heartRateHandler.getBatteryLevel() != null
|
||||
? BatteryLevel.of(heartRateHandler.getBatteryLevel()) : null;
|
||||
|
||||
return new SensorDataSet(
|
||||
gpsSensorData,
|
||||
getSpeed(now),
|
||||
@@ -255,7 +267,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
temperatureHandler.getSensorData(now),
|
||||
getCadence(now),
|
||||
cyclingPowerHandler.getSensorData(now),
|
||||
altitudeChangeHandler.getSensorData(now)
|
||||
altitudeChangeHandler.getSensorData(now),
|
||||
cadenceBattery,
|
||||
speedBattery,
|
||||
heartRateBattery
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
@@ -53,6 +54,9 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
private String address;
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
@@ -68,10 +72,21 @@ public class BluetoothDriver<T> implements Driver {
|
||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
//This is also triggered, if no connection was established (ca. 30s)
|
||||
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
||||
if (gatt.connect()) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
|
||||
}
|
||||
observer.onConnectionLost();
|
||||
|
||||
synchronized (BluetoothDriver.this) {
|
||||
gatt.close();
|
||||
if (bluetoothGatt == gatt) {
|
||||
bluetoothGatt = null;
|
||||
}
|
||||
|
||||
if (context != null && handler != null && address != null) {
|
||||
final Context ctx = context;
|
||||
final Handler hnd = handler;
|
||||
final String addr = address;
|
||||
hnd.post(() -> connect(ctx, hnd, addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,9 +131,39 @@ public class BluetoothDriver<T> implements Driver {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
||||
}
|
||||
|
||||
// NOTE: battery read is triggered in onDescriptorWrite, after this write completes.
|
||||
// Attempting readCharacteristic here would fail silently because GATT
|
||||
// allows only one pending operation at a time.
|
||||
|
||||
observer.onConnected(device.getAddress(), device.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
// After the notification descriptor write completes, try to read the battery level.
|
||||
// This is the correct moment — GATT is idle and can process a new operation.
|
||||
BluetoothGattService batteryService = gatt.getService(BluetoothUtils.BATTERY.serviceUUID());
|
||||
if (batteryService != null) {
|
||||
BluetoothGattCharacteristic batteryChar = batteryService.getCharacteristic(BluetoothUtils.BATTERY.measurementUUID());
|
||||
if (batteryChar != null) {
|
||||
boolean queued = gatt.readCharacteristic(batteryChar);
|
||||
Log.d(TAG, gatt.getDevice() + ": battery read queued=" + queued);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, int status) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS
|
||||
&& characteristic.getUuid().equals(BluetoothUtils.BATTERY.measurementUUID())) {
|
||||
BatteryLevel level = BluetoothUtils.parseBatteryLevel(characteristic);
|
||||
if (level != null) {
|
||||
Log.d(TAG, gatt.getDevice() + ": battery level " + level.percentage() + "%");
|
||||
observer.onBatteryLevelReceived(level.percentage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
UUID serviceUUID = characteristic.getService().getUuid();
|
||||
@@ -169,6 +214,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
Log.d(TAG, device + ": trying to connect");
|
||||
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
this.address = address;
|
||||
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||
}
|
||||
|
||||
@@ -184,6 +233,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||
bluetoothGatt = null;
|
||||
|
||||
this.context = null;
|
||||
this.handler = null;
|
||||
this.address = null;
|
||||
|
||||
observer.onDisconnected();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,10 @@ public interface DriverObserver<T> {
|
||||
* For example, the user deactivated the internal GPS.
|
||||
*/
|
||||
default void onSensorDeactivated() {}
|
||||
|
||||
/**
|
||||
* The Driver read the battery level from the Battery Service (0x180F).
|
||||
* Only called if the device exposes the Battery Service.
|
||||
*/
|
||||
default void onBatteryLevelReceived(int percentage) {}
|
||||
}
|
||||
|
||||
+15
-2
@@ -5,6 +5,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
@@ -18,17 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
|
||||
if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
|
||||
output = directCadenceData.cadence();
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value());
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value(), current.time());
|
||||
}
|
||||
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw) {
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw, Instant now) {
|
||||
CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw();
|
||||
if (previousRaw == null) return;
|
||||
|
||||
@@ -36,6 +40,9 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, now).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,9 +59,15 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
lastValidUpdate = now;
|
||||
output = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetOutdated() {
|
||||
output = Cadence.of(0f);
|
||||
}
|
||||
|
||||
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
|
||||
if (previous == null) {
|
||||
return null;
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
@@ -21,9 +22,12 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
||||
if (previous == null) {
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +35,9 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, current.time()).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,6 +59,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
distanceOverall = distance.plus(output.distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
lastValidUpdate = current.time();
|
||||
output = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
@@ -27,6 +28,12 @@ public record SensorDataSet(
|
||||
@Nullable
|
||||
SensorData<Power> power,
|
||||
@Nullable
|
||||
SensorData<AltitudeGainLoss> altitudeGainLoss
|
||||
SensorData<AltitudeGainLoss> altitudeGainLoss,
|
||||
@Nullable
|
||||
BatteryLevel cadenceBattery,
|
||||
@Nullable
|
||||
BatteryLevel speedBattery,
|
||||
@Nullable
|
||||
BatteryLevel heartRateBattery
|
||||
) {
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MediaNotificationListenerService extends NotificationListenerService {
|
||||
|
||||
private static final String TAG = MediaNotificationListenerService.class.getSimpleName();
|
||||
|
||||
private MediaSessionManager mediaSessionManager;
|
||||
private MediaController currentController;
|
||||
|
||||
private static final MutableLiveData<MediaData> mediaDataLiveData = new MutableLiveData<>(new MediaData(null, null, false, false));
|
||||
private static MediaNotificationListenerService instance;
|
||||
|
||||
public static LiveData<MediaData> getMediaDataObservable() {
|
||||
return mediaDataLiveData;
|
||||
}
|
||||
|
||||
public static MediaNotificationListenerService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final MediaController.Callback mediaControllerCallback = new MediaController.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackState state) {
|
||||
updateMediaData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataChanged(MediaMetadata metadata) {
|
||||
updateMediaData();
|
||||
}
|
||||
};
|
||||
|
||||
private final MediaSessionManager.OnActiveSessionsChangedListener sessionsChangedListener = controllers -> {
|
||||
Log.d(TAG, "Active sessions changed");
|
||||
findActiveMediaController(controllers);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
mediaSessionManager = (MediaSessionManager) getSystemService(MEDIA_SESSION_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerConnected() {
|
||||
super.onListenerConnected();
|
||||
Log.d(TAG, "Listener connected");
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||
try {
|
||||
ComponentName componentName = new ComponentName(this, MediaNotificationListenerService.class);
|
||||
mediaSessionManager.addOnActiveSessionsChangedListener(sessionsChangedListener, componentName);
|
||||
findActiveMediaController(mediaSessionManager.getActiveSessions(componentName));
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "SecurityException while accessing MediaSessionManager", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListenerDisconnected() {
|
||||
super.onListenerDisconnected();
|
||||
Log.d(TAG, "Listener disconnected");
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, false));
|
||||
if (mediaSessionManager != null) {
|
||||
mediaSessionManager.removeOnActiveSessionsChangedListener(sessionsChangedListener);
|
||||
}
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
currentController = null;
|
||||
}
|
||||
instance = null;
|
||||
}
|
||||
|
||||
private void findActiveMediaController(List<MediaController> controllers) {
|
||||
if (controllers == null || controllers.isEmpty()) {
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
currentController = null;
|
||||
}
|
||||
updateMediaData();
|
||||
return;
|
||||
}
|
||||
|
||||
MediaController newController = controllers.get(0);
|
||||
for (MediaController controller : controllers) {
|
||||
PlaybackState state = controller.getPlaybackState();
|
||||
if (state != null && (state.getState() == PlaybackState.STATE_PLAYING || state.getState() == PlaybackState.STATE_BUFFERING)) {
|
||||
newController = controller;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentController != null && currentController.getSessionToken().equals(newController.getSessionToken())) {
|
||||
// Already tracking this controller
|
||||
updateMediaData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentController != null) {
|
||||
currentController.unregisterCallback(mediaControllerCallback);
|
||||
}
|
||||
|
||||
currentController = newController;
|
||||
currentController.registerCallback(mediaControllerCallback);
|
||||
updateMediaData();
|
||||
}
|
||||
|
||||
private void updateMediaData() {
|
||||
if (currentController == null) {
|
||||
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||
return;
|
||||
}
|
||||
|
||||
MediaMetadata metadata = currentController.getMetadata();
|
||||
PlaybackState playbackState = currentController.getPlaybackState();
|
||||
|
||||
String title = null;
|
||||
String artist = null;
|
||||
if (metadata != null) {
|
||||
title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
|
||||
artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
|
||||
}
|
||||
|
||||
boolean isPlaying = false;
|
||||
if (playbackState != null) {
|
||||
isPlaying = playbackState.getState() == PlaybackState.STATE_PLAYING ||
|
||||
playbackState.getState() == PlaybackState.STATE_BUFFERING;
|
||||
}
|
||||
|
||||
mediaDataLiveData.postValue(new MediaData(title, artist, isPlaying, true));
|
||||
}
|
||||
|
||||
public void sendMediaCommand(int keyCode) {
|
||||
if (currentController != null) {
|
||||
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
} else {
|
||||
// Fallback if no active controller (will broadcast to the system)
|
||||
android.media.AudioManager audioManager = (android.media.AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager != null) {
|
||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MediaData {
|
||||
public final String title;
|
||||
public final String artist;
|
||||
public final boolean isPlaying;
|
||||
public final boolean isPermissionGranted;
|
||||
|
||||
public MediaData(String title, String artist, boolean isPlaying, boolean isPermissionGranted) {
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.isPlaying = isPlaying;
|
||||
this.isPermissionGranted = isPermissionGranted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,13 @@ public record RecordingData(
|
||||
*/
|
||||
TrackPoint latestTrackPoint,
|
||||
SensorDataSet sensorDataSet,
|
||||
Statistics currentSegment) {
|
||||
Statistics currentSegment,
|
||||
String autoPauseStatus,
|
||||
String idleTimer,
|
||||
String walkingTimer,
|
||||
String markerTimer) {
|
||||
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null);
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null, "", "", "", "");
|
||||
|
||||
public Statistics trackStatistics() {
|
||||
return track.statistics();
|
||||
|
||||
@@ -36,10 +36,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
|
||||
|
||||
private final Runnable ON_IDLE = this::onIdle;
|
||||
private final Runnable ON_LONG_BREAK = this::onLongBreak;
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final Context context;
|
||||
private final IdleObserver idleObserver;
|
||||
private final SensorDisconnectObserver sensorDisconnectObserver;
|
||||
|
||||
private final Handler handler;
|
||||
|
||||
@@ -59,9 +61,25 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private TrackPoint lastStoredTrackPoint;
|
||||
private TrackPoint lastStoredTrackPointWithLocation;
|
||||
|
||||
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, Handler handler) {
|
||||
private boolean hasSensorBeenConnected = false;
|
||||
private boolean isSensorDisconnectedWarningActive = false;
|
||||
|
||||
private boolean autoPauseOnCadence0;
|
||||
private de.dennisguse.opentracks.data.models.Speed autoPauseCadence0Speed;
|
||||
private boolean hasCadenceSensorBeenConnected = false;
|
||||
private String autoPauseStatus = "N/A";
|
||||
|
||||
private java.time.Instant idleTimeoutScheduledAt;
|
||||
private java.time.Instant walkingStartedAt;
|
||||
private java.time.Instant idleStartedAt;
|
||||
private boolean longBreakRecorded = false;
|
||||
|
||||
private boolean gpsOverriden = false;
|
||||
|
||||
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, SensorDisconnectObserver sensorDisconnectObserver, Handler handler) {
|
||||
this.context = context;
|
||||
this.idleObserver = idleObserver;
|
||||
this.sensorDisconnectObserver = sensorDisconnectObserver;
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
this.handler = handler;
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -97,6 +115,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
onNewTrackPoint(segmentStartTrackPoint);
|
||||
|
||||
handler.post(() -> {
|
||||
if (trackId != null) {
|
||||
onIdle();
|
||||
}
|
||||
});
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
handler.postDelayed(() -> {
|
||||
if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return trackId;
|
||||
}
|
||||
|
||||
@@ -116,6 +151,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
reset();
|
||||
|
||||
handler.post(() -> {
|
||||
if (this.trackId != null) {
|
||||
onIdle();
|
||||
}
|
||||
});
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
handler.postDelayed(() -> {
|
||||
if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,11 +178,21 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
trackId = null;
|
||||
trackStatisticsUpdater = null;
|
||||
|
||||
idleTimeoutScheduledAt = null;
|
||||
walkingStartedAt = null;
|
||||
idleStartedAt = null;
|
||||
longBreakRecorded = false;
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
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);
|
||||
TrackPoint trackPoint = current.first;
|
||||
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater, trackPoint);
|
||||
@@ -139,38 +201,120 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
Track track = contentProviderUtils.getTrack(trackId); //Get copy TODO: Avoid re-loading data from DB
|
||||
if (track == null) {
|
||||
Log.w(TAG, "Requesting data if not recording is taking place, should not be done.");
|
||||
return null;
|
||||
throw new RuntimeException("Data could not be loaded for TrackId " + trackId);
|
||||
}
|
||||
|
||||
TrackBuilder trackBuilder = new TrackBuilder(track);
|
||||
trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
|
||||
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
String idleTimer = "";
|
||||
if (idleTimeoutScheduledAt != null && !"IDLE".equals(autoPauseStatus) && !"WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(now, idleTimeoutScheduledAt);
|
||||
if (!d.isNegative()) idleTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String walkingTimer = "";
|
||||
if (walkingStartedAt != null && "WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(walkingStartedAt, now);
|
||||
if (!d.isNegative()) walkingTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String markerTimer = "";
|
||||
if (idleStartedAt != null && "IDLE".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(idleStartedAt, now);
|
||||
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||
if (longBreakRecorded || d.compareTo(markerDuration) >= 0) {
|
||||
d = markerDuration;
|
||||
}
|
||||
if (!d.isNegative()) markerTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment(), autoPauseStatus, idleTimer, walkingTimer, markerTimer);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public synchronized void onIdle() {
|
||||
Log.d(TAG, "Becoming idle");
|
||||
onNewTrackPoint(trackPointCreator.createIdle());
|
||||
TrackPoint idlePoint = trackPointCreator.createIdle();
|
||||
|
||||
boolean preventIdle = de.dennisguse.opentracks.settings.PreferencesUtils.shouldPreventIdleIfCadence();
|
||||
if (preventIdle && hasCadenceSensorBeenConnected) {
|
||||
if (idlePoint.cadence() != null && idlePoint.cadence().value_rpm() > 0) {
|
||||
scheduleNewIdleTimeout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!"IDLE".equals(autoPauseStatus)) {
|
||||
idleStartedAt = java.time.Instant.now();
|
||||
longBreakRecorded = false;
|
||||
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
handler.postDelayed(ON_LONG_BREAK, markerDuration.toMillis());
|
||||
}
|
||||
autoPauseStatus = "IDLE";
|
||||
|
||||
onNewTrackPoint(idlePoint);
|
||||
|
||||
idleObserver.onIdle();
|
||||
}
|
||||
|
||||
private synchronized void onLongBreak() {
|
||||
if ("IDLE".equals(autoPauseStatus) && idleStartedAt != null && !longBreakRecorded) {
|
||||
longBreakRecorded = true;
|
||||
TrackPoint trackPoint = trackPointCreator.createSegmentStartManual();
|
||||
insertTrackPoint(trackPoint, true);
|
||||
idleObserver.onLongBreak();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TrackPoint was stored?
|
||||
*/
|
||||
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
lastTrackPointUIWithAltitude = trackPoint;
|
||||
}
|
||||
|
||||
boolean sensorHasDistance = trackPoint.sensorDistance() != null;
|
||||
if (sensorHasDistance) {
|
||||
hasSensorBeenConnected = true;
|
||||
if (isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
sensorDisconnectObserver.onSensorConnected();
|
||||
}
|
||||
} else if (hasSensorBeenConnected) {
|
||||
if (!isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
boolean isSensorExpected = !de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address);
|
||||
boolean strictSensorMode = isSensorExpected && !sensorHasDistance && !gpsOverriden;
|
||||
if (strictSensorMode) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
trackPoint = trackPoint.with(trackPoint.position().with(de.dennisguse.opentracks.data.models.Speed.ZERO));
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
|
||||
|
||||
if (trackPoint.type() == TrackPoint.Type.IDLE) {
|
||||
longBreakRecorded = false;
|
||||
insertTrackPoint(trackPoint, true);
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
|
||||
long durationMs = PreferencesUtils.getMarkerIdleDuration().toMillis();
|
||||
if (durationMs > 0) {
|
||||
handler.postDelayed(ON_LONG_BREAK, durationMs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//Storing trackPoint
|
||||
@@ -201,11 +345,37 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.cadence() != null) {
|
||||
hasCadenceSensorBeenConnected = true;
|
||||
}
|
||||
|
||||
boolean isWalkingWithBike = false;
|
||||
if (autoPauseOnCadence0 && hasCadenceSensorBeenConnected && autoPauseCadence0Speed != null) {
|
||||
if (trackPoint.cadence() == null || trackPoint.cadence().getRPM() == 0) {
|
||||
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0 && trackPoint.position().speed().lessThan(autoPauseCadence0Speed)) {
|
||||
isWalkingWithBike = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWalkingWithBike && "WALKING".equals(autoPauseStatus)) {
|
||||
autoPauseStatus = "REC";
|
||||
walkingStartedAt = null;
|
||||
scheduleNewIdleTimeout();
|
||||
}
|
||||
|
||||
Distance distanceToLastStoredTrackPoint;
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
if (isWalkingWithBike || strictSensorMode) {
|
||||
distanceToLastStoredTrackPoint = Distance.ZERO;
|
||||
if (isWalkingWithBike) {
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||
@@ -213,6 +383,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
if (!isWalkingWithBike) {
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
autoPauseStatus = "REC";
|
||||
} else {
|
||||
if (!"WALKING".equals(autoPauseStatus)) {
|
||||
walkingStartedAt = java.time.Instant.now();
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
}
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -220,6 +404,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, false);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
if (!isWalkingWithBike) {
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
autoPauseStatus = "REC";
|
||||
} else {
|
||||
if (!"WALKING".equals(autoPauseStatus)) {
|
||||
walkingStartedAt = java.time.Instant.now();
|
||||
if ("IDLE".equals(autoPauseStatus)) {
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
}
|
||||
}
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -236,6 +434,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
||||
idleTimeoutScheduledAt = java.time.Instant.now().plus(idleDuration);
|
||||
}
|
||||
|
||||
Statistics getTrackStatistics() {
|
||||
@@ -285,6 +484,13 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
lastStoredTrackPoint = null;
|
||||
lastStoredTrackPointWithLocation = null;
|
||||
|
||||
hasSensorBeenConnected = false;
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
hasCadenceSensorBeenConnected = false;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -298,13 +504,36 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
|
||||
idleDuration = PreferencesUtils.getIdleDurationTimeout();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_key, key)) {
|
||||
autoPauseOnCadence0 = PreferencesUtils.shouldAutoPauseOnCadence0();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, key)) {
|
||||
autoPauseCadence0Speed = PreferencesUtils.getAutoPauseCadence0Speed();
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return lastStoredTrackPointWithLocation;
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (override) {
|
||||
hasSensorBeenConnected = false;
|
||||
gpsOverriden = true;
|
||||
if (isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IdleObserver {
|
||||
void onIdle();
|
||||
void onLongBreak();
|
||||
}
|
||||
|
||||
public interface SensorDisconnectObserver {
|
||||
void onSensorDisconnectedWarning(boolean show);
|
||||
void onSensorConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver, TrackRecordingManager.SensorDisconnectObserver {
|
||||
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
|
||||
@@ -84,6 +84,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
private RecordingStatus recordingStatus;
|
||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||
private MutableLiveData<Boolean> sensorDisconnectedWarningObservable;
|
||||
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||
|
||||
private Handler handler;
|
||||
@@ -104,10 +105,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
recordingStatusObservable = new MutableLiveData<>();
|
||||
updateRecordingStatus(STATUS_DEFAULT);
|
||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||
sensorDisconnectedWarningObservable = new MutableLiveData<>(false);
|
||||
recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING);
|
||||
|
||||
trackPointCreator = new TrackPointCreator(this);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, this, handler);
|
||||
|
||||
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
|
||||
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
||||
@@ -138,6 +140,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
recordingStatusObservable = null;
|
||||
gpsStatusObservable = null;
|
||||
sensorDisconnectedWarningObservable = null;
|
||||
recordingDataObservable = null;
|
||||
|
||||
Log.d(TAG, "Destroyed");
|
||||
@@ -315,6 +318,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
return gpsStatusObservable;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getSensorDisconnectedWarningObservable() {
|
||||
return sensorDisconnectedWarningObservable;
|
||||
}
|
||||
|
||||
public LiveData<RecordingData> getRecordingDataObservable() {
|
||||
return recordingDataObservable;
|
||||
}
|
||||
@@ -337,6 +344,32 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
voiceAnnouncementManager.announceIdle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongBreak() {
|
||||
voiceAnnouncementManager.announceLongBreak();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorDisconnectedWarning(boolean show) {
|
||||
if (show) {
|
||||
voiceAnnouncementManager.announceSensorDisconnected();
|
||||
}
|
||||
if (sensorDisconnectedWarningObservable != null) {
|
||||
sensorDisconnectedWarningObservable.postValue(show);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorConnected() {
|
||||
voiceAnnouncementManager.announceSensorConnected();
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (trackRecordingManager != null) {
|
||||
trackRecordingManager.setOverrideSensorWithGPS(override);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void stopUpdateRecordingData() {
|
||||
handler.removeCallbacks(updateRecordingData);
|
||||
|
||||
+24
@@ -119,6 +119,30 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
|
||||
}
|
||||
|
||||
public void announceLongBreak() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementLongBreak()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createLongBreak(context));
|
||||
}
|
||||
|
||||
public void announceSensorDisconnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorDisconnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorDisconnected(context));
|
||||
}
|
||||
|
||||
public void announceSensorConnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorConnected(context));
|
||||
}
|
||||
|
||||
public void announceStatisticsIfNeeded(@NonNull RecordingData data) {
|
||||
if (shouldNotAnnounce()) {
|
||||
return;
|
||||
|
||||
+15
@@ -50,6 +50,21 @@ class VoiceAnnouncementUtils {
|
||||
.append(context.getString(R.string.voiceIdle));
|
||||
}
|
||||
|
||||
static Spannable createLongBreak(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceLongBreak));
|
||||
}
|
||||
|
||||
static Spannable createSensorDisconnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorDisconnected));
|
||||
}
|
||||
|
||||
static Spannable createSensorConnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorConnected));
|
||||
}
|
||||
|
||||
static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
Statistics trackStatistics = track.statistics();
|
||||
|
||||
|
||||
@@ -101,8 +101,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
|
||||
}
|
||||
|
||||
if (lastTrackPointUISpeed != null)
|
||||
if (sensorDataSet.speed() != null) {
|
||||
position = position.with(sensorDataSet.speed().data());
|
||||
} else if (lastTrackPointUISpeed != null) {
|
||||
position = position.with(lastTrackPointUISpeed.position().speed());
|
||||
}
|
||||
|
||||
if (lastTrackPointUIAltitude != null)
|
||||
position = position.with(lastTrackPointUIAltitude.position().altitude());
|
||||
|
||||
@@ -38,11 +38,13 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -248,6 +250,10 @@ public class PreferencesUtils {
|
||||
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
|
||||
}
|
||||
|
||||
public static Duration getMarkerIdleDuration() {
|
||||
return Duration.ofMinutes(getInt(R.string.marker_idle_duration_key, 3));
|
||||
}
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||
@@ -341,6 +347,20 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.voice_announce_idle_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||
return getBoolean(R.string.voice_announce_long_break_key, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorDisconnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_disconnected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorConnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_connected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceTime() {
|
||||
return getBoolean(R.string.voice_announce_time_key, true);
|
||||
}
|
||||
@@ -688,6 +708,51 @@ public class PreferencesUtils {
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getMarkerIdleDurationEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.marker_idle_duration_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int defaultDuration = Integer.parseInt(resources.getString(R.string.marker_idle_duration_default));
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
|
||||
if (value == defaultDuration) {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute_recommended, value);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute, value);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldAutoPauseOnCadence0() {
|
||||
return getBoolean(R.string.settings_recording_auto_pause_on_cadence_0_key, false);
|
||||
}
|
||||
|
||||
public static boolean shouldPreventIdleIfCadence() {
|
||||
return getBoolean(R.string.settings_recording_prevent_idle_if_cadence_key, true);
|
||||
}
|
||||
|
||||
public static Speed getAutoPauseCadence0Speed() {
|
||||
final int DEFAULT = Integer.parseInt(resources.getString(R.string.auto_pause_cadence_0_speed_default));
|
||||
int value = getInt(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, DEFAULT);
|
||||
return Speed.ofKMH(value);
|
||||
}
|
||||
|
||||
static String[] getAutoPauseCadence0SpeedEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.auto_pause_cadence_0_speed_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
entries[i] = value + " km/h";
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldInstantExportToFile() {
|
||||
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default);
|
||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||
@@ -845,7 +910,15 @@ public class PreferencesUtils {
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_cadence_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_power_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_clock_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false)
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_auto_pause_status_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_hr_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_cadence_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_speed_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_idle_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_walking_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_marker_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_media_key), false, false, true)
|
||||
);
|
||||
|
||||
RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2);
|
||||
@@ -861,7 +934,7 @@ public class PreferencesUtils {
|
||||
String selectedLayout = getSelectedLayoutName();
|
||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||
|
||||
List<RecordingLayout> recordingLayouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<RecordingLayout> recordingLayouts = getAllCustomLayouts();
|
||||
|
||||
RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout();
|
||||
|
||||
@@ -907,7 +980,18 @@ public class PreferencesUtils {
|
||||
public static List<RecordingLayout> getAllCustomLayouts() {
|
||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||
|
||||
return RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<RecordingLayout> layouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||
List<DataField> defaultFields = getDefaultLayout().getFields();
|
||||
|
||||
for (RecordingLayout layout : layouts) {
|
||||
for (DataField df : defaultFields) {
|
||||
boolean hasField = layout.getFields().stream().anyMatch(f -> f.getKey().equals(df.getKey()));
|
||||
if (!hasField) {
|
||||
layout.addField(new DataField(df.getKey(), false, false, df.isWide()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return layouts;
|
||||
}
|
||||
|
||||
public static void resetCustomLayoutPreferences() {
|
||||
|
||||
@@ -57,5 +57,15 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
|
||||
idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries());
|
||||
|
||||
ListPreference autoPauseCadence0Speed = findPreference(getString(R.string.settings_recording_auto_pause_on_cadence_0_speed_key));
|
||||
if (autoPauseCadence0Speed != null) {
|
||||
autoPauseCadence0Speed.setEntries(PreferencesUtils.getAutoPauseCadence0SpeedEntries());
|
||||
}
|
||||
|
||||
ListPreference markerIdleDuration = findPreference(getString(R.string.marker_idle_duration_key));
|
||||
if (markerIdleDuration != null) {
|
||||
markerIdleDuration.setEntries(PreferencesUtils.getMarkerIdleDurationEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -40,6 +40,13 @@ public class RecordingLayoutIO {
|
||||
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
|
||||
return recordingLayout;
|
||||
}
|
||||
|
||||
String key = fieldParts[0];
|
||||
if (key.equals(resources.getString(R.string.stats_custom_layout_sensor_status_key))) {
|
||||
// Ignore deprecated sensor_status key to safely remove it from user's custom layout
|
||||
continue;
|
||||
}
|
||||
|
||||
recordingLayout.addField(fromCSV(fieldParts, resources));
|
||||
}
|
||||
return recordingLayout;
|
||||
@@ -50,11 +57,24 @@ public class RecordingLayoutIO {
|
||||
}
|
||||
|
||||
private static DataField fromCSV(String[] fieldParts, @NonNull Resources resources) {
|
||||
boolean isWide;
|
||||
String key = fieldParts[0];
|
||||
|
||||
// Force isWide to true for specific keys that must always be wide
|
||||
if (key.equals(resources.getString(R.string.stats_custom_layout_coordinates_key)) ||
|
||||
key.equals(resources.getString(R.string.stats_custom_layout_media_key))) {
|
||||
isWide = true;
|
||||
} else if (fieldParts.length > 3) {
|
||||
isWide = YES_VALUE.equals(fieldParts[3]);
|
||||
} else {
|
||||
isWide = false;
|
||||
}
|
||||
|
||||
return new DataField(
|
||||
fieldParts[0],
|
||||
key,
|
||||
YES_VALUE.equals(fieldParts[1]),
|
||||
YES_VALUE.equals(fieldParts[2]),
|
||||
fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
|
||||
isWide);
|
||||
}
|
||||
|
||||
static String toCsv(DataField datafield) {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<Recycl
|
||||
Speed.ZERO
|
||||
);
|
||||
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics);
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics, "", "", "", "");
|
||||
}
|
||||
|
||||
private RecordingLayout recordingLayout;
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
@@ -288,4 +289,86 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
getBinding().statsDescriptionMain.setText(R.string.stats_coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AutoPauseStatus extends GenericStatisticsViewHolder {
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String value = data.autoPauseStatus();
|
||||
if (value == null || value.isEmpty()) {
|
||||
value = "N/A";
|
||||
}
|
||||
getBinding().statsValue.setText(value);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_auto_pause_status));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusHR extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().heartRate() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().heartRateBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_hr));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusCadence extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().cadence() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().cadenceBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_cadence));
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorStatusSpeed extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String status = getContext().getString(R.string.value_unknown);
|
||||
if (data.sensorDataSet() != null && data.sensorDataSet().speed() != null) {
|
||||
BatteryLevel battery = data.sensorDataSet().speedBattery();
|
||||
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||
}
|
||||
getBinding().statsValue.setText(status);
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_speed));
|
||||
}
|
||||
}
|
||||
public static class DebugIdleTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.idleTimer().isEmpty() ? "---" : data.idleTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_idle_timer));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugWalkingTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.walkingTimer().isEmpty() ? "---" : data.walkingTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_walking_timer));
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugMarkerTimer extends GenericStatisticsViewHolder {
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
getBinding().statsValue.setText(data.markerTimer().isEmpty() ? "---" : data.markerTimer());
|
||||
getBinding().statsUnit.setText("");
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_marker_timer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ public class Mapping {
|
||||
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_auto_pause_status_key), GenericStatisticsViewHolder.AutoPauseStatus::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_hr_key), GenericStatisticsViewHolder.SensorStatusHR::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_cadence_key), GenericStatisticsViewHolder.SensorStatusCadence::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_sensor_status_speed_key), GenericStatisticsViewHolder.SensorStatusSpeed::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_idle_timer_key), GenericStatisticsViewHolder.DebugIdleTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_walking_timer_key), GenericStatisticsViewHolder.DebugWalkingTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_debug_marker_timer_key), GenericStatisticsViewHolder.DebugMarkerTimer::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_media_key), MediaViewHolder::new);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.provider.Settings;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.databinding.StatsMediaItemBinding;
|
||||
import de.dennisguse.opentracks.services.MediaNotificationListenerService;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
|
||||
|
||||
public class MediaViewHolder extends StatisticViewHolder<StatsMediaItemBinding> {
|
||||
|
||||
@Override
|
||||
protected StatsMediaItemBinding createViewBinding(LayoutInflater inflater) {
|
||||
return StatsMediaItemBinding.inflate(inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureUI(DataField dataField) {
|
||||
getBinding().statsMediaPrev.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaNext.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_NEXT);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaPlayPause.setOnClickListener(v -> {
|
||||
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||
if (service != null) {
|
||||
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
|
||||
}
|
||||
});
|
||||
|
||||
getBinding().statsMediaPermissionButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
});
|
||||
|
||||
if (getContext() instanceof LifecycleOwner) {
|
||||
MediaNotificationListenerService.getMediaDataObservable().observe((LifecycleOwner) getContext(), mediaData -> {
|
||||
if (mediaData.isPermissionGranted) {
|
||||
getBinding().statsMediaPermissionButton.setVisibility(View.GONE);
|
||||
getBinding().statsMediaControls.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaTitle.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaArtist.setVisibility(View.VISIBLE);
|
||||
|
||||
getBinding().statsMediaTitle.setText(mediaData.title != null ? mediaData.title : getContext().getString(R.string.media_title_default));
|
||||
getBinding().statsMediaArtist.setText(mediaData.artist != null ? mediaData.artist : getContext().getString(R.string.media_artist_default));
|
||||
|
||||
if (mediaData.isPlaying) {
|
||||
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_pause_24); // Needs to exist, or fallback to something else
|
||||
} else {
|
||||
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_play_arrow_24);
|
||||
}
|
||||
} else {
|
||||
getBinding().statsMediaPermissionButton.setVisibility(View.VISIBLE);
|
||||
getBinding().statsMediaControls.setVisibility(View.GONE);
|
||||
getBinding().statsMediaTitle.setVisibility(View.GONE);
|
||||
getBinding().statsMediaArtist.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
// Not used, as we observe LiveData directly
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,19h4V5H6v14zm8,-14v14h4V5h-4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,6h2v12H6zm3.5,6l8.5,6V6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_media_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/media_title_default"
|
||||
android:textAppearance="@style/TextAppearance.OpenTracks.PrimaryValue"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_media_artist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/media_artist_default"
|
||||
android:textAppearance="@style/TextAppearance.OpenTracks.SecondaryHeader"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_media_title" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/stats_media_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_media_artist"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_prev"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_prev"
|
||||
android:src="@drawable/ic_baseline_skip_previous_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_play_pause"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_play_pause"
|
||||
android:src="@drawable/ic_baseline_play_arrow_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stats_media_next"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/media_next"
|
||||
android:src="@drawable/ic_baseline_skip_next_24"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/stats_media_permission_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_permission_request"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
--><resources>
|
||||
<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_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_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>
|
||||
<!-- Help -->
|
||||
<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* 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_content"> Utilice el pequeño icono de mapa.
|
||||
\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_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>
|
||||
<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_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_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_content"> Lo que puedes hacer:
|
||||
\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_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>
|
||||
<string name="help_support_title">¿Cómo puedo ayudar a OpenTracks\?</string>
|
||||
<string name="help_support_content"> Usted puede:
|
||||
\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>
|
||||
<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>
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">avión</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_system_default">Predeterminado del sistema</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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -197,6 +197,7 @@ limitations under the License.
|
||||
<string name="settings_recording_max_recording_distance_summary">Nowy segment jest tworzony, jeśli odległość między dwiema lokalizacjami jest większa niż %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Maksymalna odległość</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Rejestrowanie odcinka</string>
|
||||
<string name="settings_recording_marker_idle_duration_title">Minimalny czas długiej przerwy</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Rejestrowanie interwału</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">Dokładność GPS-u</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
|
||||
@@ -281,6 +282,7 @@ limitations under the License.
|
||||
<string name="value_integer_meter_recommended">%1$d m (zalecane)</string>
|
||||
<string name="value_integer_mile_hour">%1$d mil/h</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_minute_recommended">%1$d min (zalecane)</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
<string name="value_none">Brak</string>
|
||||
<string name="value_off">Wyłącz</string>
|
||||
@@ -443,6 +445,8 @@ limitations under the License.
|
||||
<string name="settings_announcements_totaltime_frequency">Przedział czasu</string>
|
||||
<string name="stats_sensors_heart_rate">Tętno</string>
|
||||
<string name="stats_clock">Zegar</string>
|
||||
<string name="stats_auto_pause_status">Status auto-pauzy</string>
|
||||
<string name="stats_sensor_status">Status czujników</string>
|
||||
<string name="settings_announcements_distance_frequency">Długość okrążenia</string>
|
||||
<string name="settings_layout_reset_confirm_title">Zresetować twoje układy\?</string>
|
||||
<string name="settings_cycling_sensor">Kolarstwo</string>
|
||||
@@ -523,6 +527,10 @@ limitations under the License.
|
||||
<string name="settings_announcements_lap_speed_pace">Tempo/prędkość okrążenia</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d ścieżki</string>
|
||||
<string name="settings_announcements_idle_title">Przerwa</string>
|
||||
<string name="settings_announcements_long_break_title">Długa przerwa (filiżanka)</string>
|
||||
<string name="settings_announcements_sensor_category_title">Czujniki</string>
|
||||
<string name="settings_announcements_sensor_disconnected_title">Odłączono czujnik</string>
|
||||
<string name="settings_announcements_sensor_connected_title">Podłączono czujnik</string>
|
||||
<string name="settings_recording_title">Nagrywanie</string>
|
||||
<string name="settings_theme_switch_restart">Zmiana motywu może wymagać ręcznego restartu.</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Użyj ręcznie zdefiniowanych Dynamicznych Kolorów (tylko Android 12+; wymaga ręcznego restartu)</string>
|
||||
@@ -585,4 +593,15 @@ limitations under the License.
|
||||
<string name="show_on_dashboard">API panelu OpenTracks</string>
|
||||
<string name="marker_detail_title">%1$d z %2$d: %3$s</string>
|
||||
<string name="voice_on_device_speaker_title">Użyj wbudowanego głośnika</string>
|
||||
<string name="sensor_disconnected_warning">Czujnik rozłączony. Kontynuować z GPS?</string>
|
||||
<string name="sensor_disconnected_switch_action">Przełącz</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_title">Zatrzymaj przy prowadzeniu roweru</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_summary">Uruchom autopauzę jeżeli kadencja wynosi 0 a prędkość wskazuje na chód</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Maksymalna prędkość prowadzenia</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Prędkość poniżej której wykrywane jest prowadzenie roweru (gdy kadencja to 0)</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_title">Zapobiegaj pauzie podczas pedałowania</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_summary">Nie włączaj trybu postoju jeżeli pedałujesz, nawet przy braku ruchu</string>
|
||||
<string name="voiceLongBreak">Rozpoczęto długą przerwę.</string>
|
||||
<string name="voiceSensorDisconnected">Odłączono czujnik.</string>
|
||||
<string name="voiceSensorConnected">Podłączono czujnik.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
--><resources>
|
||||
<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_version_name">版本名稱:%1$s</string>
|
||||
<string name="about_version_code">版本代號:%1$d</string>
|
||||
@@ -31,14 +31,11 @@ limitations under the License.
|
||||
\n * 完全離線工作
|
||||
\n * 在地圖上顯示蹤跡(見下文)</string>
|
||||
<string name="help_track_map_title">如何在地圖顯示一個蹤跡?</string>
|
||||
<string name="help_track_map_content"> \u0020使用小的地圖圖示。
|
||||
\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_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>
|
||||
<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_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_content">否</string>
|
||||
<string name="help_recording_title">OpenTracks 是如何記錄資料的?</string>
|
||||
@@ -47,15 +44,9 @@ limitations under the License.
|
||||
如果 GPS 裝置回報的資料不准確 (例如,位置、速度、海拔),OpenTracks 對此無能為力。
|
||||
</string>
|
||||
<string name="help_data_storage_title">OpenTracks 如何儲存感測器資料?</string>
|
||||
<string name="help_data_storage_content"> 感測器資料儲存在每個記錄的位置。 就心率感測器而言,這意味著只有<i>一個</i>心率測量值被依照位置儲存。 如果您是站著不動或在室內,沒有感測器資料會被記錄(但它會被顯示)。 </string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks 沒有紀錄位置?</string>
|
||||
<string name="help_gps_troubleshooting_content"> \u0020您能做什麼:
|
||||
\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_data_storage_content">感測器資料會儲存在每個記錄的位置中。以心率感測器為例,這表示每個位置只會儲存 <i>一筆</i> 心率測量值。如果您站著不動或位於室內,將不會記錄感測器資料(但仍會顯示)。</string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks 沒有記錄位置?</string>
|
||||
<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>
|
||||
<string name="help_support_title">我如何支持 OpenTracks?</string>
|
||||
<string name="help_support_content">
|
||||
您可以:\n
|
||||
@@ -213,13 +204,13 @@ limitations under the License.
|
||||
<string name="generic_yesterday">昨日</string>
|
||||
<string name="generic_dismiss">關閉</string>
|
||||
<!-- 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_signal">正在等待 GPS 訊號</string>
|
||||
<string name="gps_disabled_msg">GPS 禁用</string>
|
||||
<string name="gps_fixed_and_ready">GPS 已修復並準備就緒</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 -->
|
||||
<string name="image_marker_count">標記數</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_default_activity_title">預設活動類型</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_min_recording_distance_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_night_mode_title">界面主題</string>
|
||||
<string name="settings_night_mode_option_system">系統</string>
|
||||
<string name="settings_night_mode_option_day">日</string>
|
||||
<string name="settings_night_mode_option_night">夜</string>
|
||||
<string name="settings_night_mode_option_day">早</string>
|
||||
<string name="settings_night_mode_option_night">晚</string>
|
||||
<string name="settings_announcements_totaltime_frequency">時間間隔</string>
|
||||
<string name="settings_announcements_distance_frequency">單圈長度</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_public_api_package_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_off">只有 OpenTracks 可以開始或停止紀錄。</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_disabled_toast">OpenTracks 公開 AIP 被禁用:它可以在設定中被啟用。</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_off">記錄的資料將不會自動與其他應用程式共用。</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_on">已啟動記錄的應用程式也可以存取已記錄的資料。</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">已記錄的資料不會自動與其他應用程式共用。</string>
|
||||
<!-- Settings import/export -->
|
||||
<string name="settings_import_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_body">我認為你可能會對這張圖片感興趣。</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="settings_default_export_uri_title">蹤跡匯出目錄</string>
|
||||
<!-- Fields -->
|
||||
<string name="field_set_primary">主要的</string>
|
||||
<string name="field_set_secondary">次要的</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="share_crash_report">分享</string>
|
||||
<string name="report_bug">回報 bug</string>
|
||||
<string name="show_on_dashboard">OpenTracks 儀表板 API</string>
|
||||
<string name="show_on_dashboard_not_installed">沒有安裝支援 OpenTracks 儀表板 API 的應用程式。</string>
|
||||
<string name="introduction_osm_dashboard">OpenTracks 本身不提供地圖。請安裝 OSMDashboard 以在地圖上檢視您的紀錄。</string>
|
||||
<string name="show_on_dashboard_not_installed">未安裝支援 OpenTracks 儀表板 API 的應用程式。</string>
|
||||
<string name="introduction_osm_dashboard">OpenTracks 本身不提供地圖功能。請安裝 OSMDashboard,以便在地圖上查看您的記錄。</string>
|
||||
<string name="voice_on_device_speaker_title">使用裝置的揚聲器</string>
|
||||
<string name="settings_stats_units_imperial_meter">英制(英里、公尺)</string>
|
||||
<string name="menu_export_all_one_file">全部匯出 (所有蹤跡於單一檔案)</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">如果定位的精度大於 %1$s,則它將被忽略</string>
|
||||
<string name="no_compatible_gallery_installed">沒有安裝相容的圖片庫應用程式</string>
|
||||
<string name="no_compatible_camera_installed">沒有安裝相容的攝影機應用程式</string>
|
||||
<string name="no_compatible_gallery_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_off">顯示所有可發現的藍芽裝置</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="create_marker_error">無法建立標記。請再次嘗試。</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="settings_charts_title">圖表</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_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="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>
|
||||
|
||||
@@ -130,6 +130,14 @@
|
||||
<string name="idle_duration_key" translatable="false">idleSpeedDuration</string>
|
||||
<string name="idle_duration_default" translatable="false">10</string>
|
||||
<string name="idle_duration_off" translatable="false">0</string>
|
||||
<string-array name="auto_pause_cadence_0_speed_values">
|
||||
<item>4</item>
|
||||
<item>6</item>
|
||||
<item>8</item>
|
||||
<item>10</item>
|
||||
<item>12</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="idle_duration_values">
|
||||
<item>@string/idle_duration_off</item>
|
||||
<item>5</item>
|
||||
@@ -141,6 +149,17 @@
|
||||
<item>120</item>
|
||||
</string-array>
|
||||
|
||||
<string name="marker_idle_duration_default" translatable="false">3</string>
|
||||
<string-array name="marker_idle_duration_values">
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>@string/marker_idle_duration_default</item>
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>15</item>
|
||||
<item>30</item>
|
||||
</string-array>
|
||||
|
||||
<string name="stats_rate_key" translatable="false">statsRate</string>
|
||||
<string name="stats_rate_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
|
||||
<string-array name="stats_rate_metric_options">
|
||||
@@ -245,6 +264,12 @@
|
||||
<bool name="voice_on_device_speaker_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_idle_key" translatable="false">voice_announce_idle_key</string>
|
||||
<bool name="voice_announce_idle_default" translatable="false">false</bool>
|
||||
<string name="voice_announce_long_break_key" translatable="false">voice_announce_long_break_key</string>
|
||||
<bool name="voice_announce_long_break_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_sensor_disconnected_key" translatable="false">voice_announce_sensor_disconnected_key</string>
|
||||
<bool name="voice_announce_sensor_disconnected_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_sensor_connected_key" translatable="false">voice_announce_sensor_connected_key</string>
|
||||
<bool name="voice_announce_sensor_connected_default" translatable="false">true</bool>
|
||||
|
||||
<string name="voice_announce_total_distance_key" translatable="false">voiceAnnounceTotalDistance</string>
|
||||
<bool name="voice_announce_total_distance_default" translatable="false">true</bool>
|
||||
@@ -381,6 +406,16 @@
|
||||
<string name="stats_custom_layout_average_cadence_key" translatable="false">average_cadence</string>
|
||||
<string name="stats_custom_layout_power_key" translatable="false">power</string>
|
||||
<string name="stats_custom_layout_clock_key" translatable="false">time</string>
|
||||
<string name="stats_custom_layout_auto_pause_status_key" translatable="false">auto_pause_status</string>
|
||||
<string name="stats_custom_layout_sensor_status_key" translatable="false">sensor_status</string>
|
||||
<string name="stats_custom_layout_sensor_status_hr_key" translatable="false">sensor_status_hr</string>
|
||||
<string name="stats_custom_layout_sensor_status_cadence_key" translatable="false">sensor_status_cadence</string>
|
||||
<string name="stats_custom_layout_sensor_status_speed_key" translatable="false">sensor_status_speed</string>
|
||||
<string name="stats_custom_layout_debug_idle_timer_key" translatable="false">debug_idle_timer</string>
|
||||
<string name="stats_custom_layout_debug_walking_timer_key" translatable="false">debug_walking_timer</string>
|
||||
<string name="stats_custom_layout_debug_marker_timer_key" translatable="false">debug_marker_timer</string>
|
||||
|
||||
<string name="stats_custom_layout_media_key" translatable="false">media_control</string>
|
||||
|
||||
<!-- Stats empty values -->
|
||||
<string name="stats_empty_value_integer" translatable="false">0</string>
|
||||
|
||||
@@ -331,6 +331,10 @@ limitations under the License.
|
||||
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
|
||||
<string name="settings_announcements_idle_category_title">Idle Announcements</string>
|
||||
<string name="settings_announcements_idle_title">Idle</string>
|
||||
<string name="settings_announcements_long_break_title">Long break</string>
|
||||
<string name="settings_announcements_sensor_category_title">Sensors</string>
|
||||
<string name="settings_announcements_sensor_disconnected_title">Sensor disconnected</string>
|
||||
<string name="settings_announcements_sensor_connected_title">Sensor connected</string>
|
||||
<string name="settings_announcements_statistics_title">Statistics Announcements</string>
|
||||
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
|
||||
<string name="settings_import_export_title">Import and Export</string>
|
||||
@@ -369,7 +373,9 @@ limitations under the License.
|
||||
<string name="settings_recording_max_recording_distance_summary">A new segment is created if the distance between two locations is greater than %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Max distance</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Recording distance interval</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Recording time interval</string>
|
||||
<string name="marker_idle_duration_key" translatable="false">marker_idle_duration</string>
|
||||
<string name="settings_recording_marker_idle_duration_title">Marker idle duration</string>
|
||||
<string name="settings_recording_min_recording_interval_key" translatable="false">recording_min_recording_interval_key</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Location is ignored if its accuracy is greater than %1$s</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">GPS accuracy</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Date (ISO 8601)</string>
|
||||
@@ -478,6 +484,14 @@ limitations under the License.
|
||||
<string name="stats_sensors_cadence">Cadence</string>
|
||||
<string name="stats_sensors_power">Power</string>
|
||||
<string name="stats_clock">Clock</string>
|
||||
<string name="stats_auto_pause_status">Auto-Pause Status</string>
|
||||
<string name="stats_sensor_status_hr">Sensor HR</string>
|
||||
<string name="stats_sensor_status_cadence">Sensor Cadence</string>
|
||||
<string name="stats_sensor_status_speed">Sensor Speed</string>
|
||||
<string name="stats_debug_idle_timer">Idle Timer</string>
|
||||
<string name="stats_debug_walking_timer">Walking Timer</string>
|
||||
<string name="stats_debug_marker_timer">Marker Timer</string>
|
||||
|
||||
<!-- Track -->
|
||||
<string name="track_name_format">Track %1$d</string>
|
||||
<string name="track_recording_notification_accuracy">Location accuracy: %1$s</string>
|
||||
@@ -536,6 +550,7 @@ limitations under the License.
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommended)</string>
|
||||
<string name="value_float_knots_recommended">%1$.1f knots (recommended)</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_minute_recommended">%1$d min (recommended)</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
<string name="value_none">None</string>
|
||||
<string name="value_off">Off</string>
|
||||
@@ -543,6 +558,9 @@ limitations under the License.
|
||||
<string name="value_smallest_recommended">Smallest (recommended)</string>
|
||||
<string name="value_int_seconds">%1$d s (recommended)</string>
|
||||
<string name="voiceIdle">Becoming idle.</string>
|
||||
<string name="voiceLongBreak">Long break recorded.</string>
|
||||
<string name="voiceSensorDisconnected">Sensor disconnected.</string>
|
||||
<string name="voiceSensorConnected">Sensor connected.</string>
|
||||
<string name="voiceHoursPlural">
|
||||
{n, plural,
|
||||
=1 {1 hour}
|
||||
@@ -705,4 +723,22 @@ limitations under the License.
|
||||
<string name="settings_announcements_lap_power">Lap power</string>
|
||||
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watts} }</string>
|
||||
<string name="power">Power</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_key" translatable="false">preventIdleIfCadence</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_title">Prevent auto-pause if pedaling</string>
|
||||
<string name="settings_recording_prevent_idle_if_cadence_summary">Do not enter idle mode while pedaling even if not moving</string>
|
||||
<string name="sensor_disconnected_warning">Sensor disconnected. Continue with GPS?</string>
|
||||
<string name="sensor_disconnected_switch_action">Switch</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_key" translatable="false">autoPauseCadence0</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_title">Auto-pause when carrying bike</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_summary">Pause recording if cadence is 0 and speed is low</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_key" translatable="false">autoPauseCadence0Speed</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Auto-pause max speed</string>
|
||||
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Speed below which walking with bike is detected (if cadence is 0)</string>
|
||||
<string name="auto_pause_cadence_0_speed_default" translatable="false">8</string> <string name="media_title_default">No media playing</string>
|
||||
<string name="media_artist_default">OpenTracks</string>
|
||||
<string name="media_prev">Previous</string>
|
||||
<string name="media_next">Next</string>
|
||||
<string name="media_play_pause">Play/Pause</string>
|
||||
<string name="media_permission_request">Enable Notification Access</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
android:defaultValue="@bool/voice_announce_idle_default"
|
||||
android:key="@string/voice_announce_idle_key"
|
||||
android:title="@string/settings_announcements_idle_title" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_long_break_default"
|
||||
android:key="@string/voice_announce_long_break_key"
|
||||
android:title="@string/settings_announcements_long_break_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_announcements_sensor_category_title">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_sensor_disconnected_default"
|
||||
android:key="@string/voice_announce_sensor_disconnected_key"
|
||||
android:title="@string/settings_announcements_sensor_disconnected_title" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_sensor_connected_default"
|
||||
android:key="@string/voice_announce_sensor_connected_key"
|
||||
android:title="@string/settings_announcements_sensor_connected_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_announcements_statistics_title">
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/settings_recording_title">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="@string/settings_recording_auto_pause_on_cadence_0_key"
|
||||
android:title="@string/settings_recording_auto_pause_on_cadence_0_title"
|
||||
android:summary="@string/settings_recording_auto_pause_on_cadence_0_summary" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="@string/settings_recording_prevent_idle_if_cadence_key"
|
||||
android:title="@string/settings_recording_prevent_idle_if_cadence_title"
|
||||
android:summary="@string/settings_recording_prevent_idle_if_cadence_summary" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/auto_pause_cadence_0_speed_default"
|
||||
android:entryValues="@array/auto_pause_cadence_0_speed_values"
|
||||
android:key="@string/settings_recording_auto_pause_on_cadence_0_speed_key"
|
||||
android:title="@string/settings_recording_auto_pause_on_cadence_0_speed_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/recording_distance_interval_default"
|
||||
android:entryValues="@array/recording_distance_interval_values"
|
||||
@@ -19,8 +38,17 @@
|
||||
<ListPreference
|
||||
android:defaultValue="@string/idle_duration_default"
|
||||
android:entryValues="@array/idle_duration_values"
|
||||
android:entries="@array/idle_duration_values"
|
||||
android:key="@string/idle_duration_key"
|
||||
android:title="@string/settings_recording_idle_timeout_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/marker_idle_duration_default"
|
||||
android:entryValues="@array/marker_idle_duration_values"
|
||||
android:entries="@array/marker_idle_duration_values"
|
||||
android:key="@string/marker_idle_duration_key"
|
||||
android:title="@string/settings_recording_marker_idle_duration_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
+1466
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user