Compare commits

..

1 Commits

Author SHA1 Message Date
Dennis Guse ae4d2da262 Heartrate: ignore "0" if sent from sensor as this is an invalid value.
On import "0" is ignored.
2026-02-10 18:42:14 +01:00
268 changed files with 1384 additions and 9048 deletions
+36
View File
@@ -0,0 +1,36 @@
name: Android Test
on:
push:
branches:
- main
pull_request:
jobs:
lint:
name: Android Lint
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Lint
shell: bash
run: ./gradlew lint
android-check:
runs-on: docker
container:
image: 'ubuntu:android-24.04'
steps:
- name: Checkout sources
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Test
shell: bash
run: ./gradlew testNightlyDebugUnitTest --stacktrace
-56
View File
@@ -1,56 +0,0 @@
name: Auto-Sync Upstream
on:
schedule:
- cron: '0 3 * * *' # Uruchamia się codziennie o 3:00 w nocy
workflow_dispatch: # Pozwala uruchomić to ręcznie z poziomu Gitea
permissions:
contents: write
jobs:
sync:
name: Sync with Upstream
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Configure Git & Fetch
run: |
git config user.name "Gitea Actions Bot"
git config user.email "actions@gitea.local"
git remote add upstream https://gitea.local.labus.uk/upstream-mirrors/OpenTracks.git
git fetch upstream main
- name: Check for updates & Attempt Merge
id: merge
run: |
# Sprawdzamy czy są jakiekolwiek nowe zmiany
if git diff --quiet HEAD upstream/main; then
echo "Repozytorium jest aktualne. Brak zmian."
echo "has_changes=false" >> $GITHUB_ENV
exit 0
fi
echo "Znaleziono nowe zmiany. Próbuję scalić..."
echo "has_changes=true" >> $GITHUB_ENV
# Próba automatycznego scalenia (merge)
if git merge upstream/main --no-edit; then
echo "Scalenie przebiegło pomyślnie!"
else
echo "Wykryto konflikty! Przerywam scalanie."
git merge --abort
exit 1 # Błąd celowy - Gitea wyśle Ci powiadomienie o failu
fi
- name: Push changes
if: env.has_changes == 'true'
run: |
git push origin main
echo "Zmiany pomyślnie wypchnięte do repozytorium!"
-78
View File
@@ -1,78 +0,0 @@
name: CI Pipeline
on:
push:
branches:
- main
pull_request:
permissions:
contents: write
jobs:
lint:
name: Android Lint
runs-on: ubuntu-latest
container:
image: 'cimg/android:2024.04-node'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Lint
shell: bash
run: ./gradlew lint
android-check:
name: Unit Tests
runs-on: ubuntu-latest
container:
image: 'cimg/android:2024.04-node'
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Test
shell: bash
run: ./gradlew testNightlyDebugUnitTest --stacktrace
release:
name: Build and Upload APK
needs: [lint, android-check]
if: github.event_name == 'push'
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
./gradlew assembleRelease
- name: Prepare APKs
shell: bash
run: |
mkdir -p release-apks
find . -name "*.apk" -print -exec cp {} release-apks/ \;
ls -la release-apks/
- name: Upload to Gitea Releases
uses: https://gitea.com/actions/gitea-release-action@v1
with:
tag_name: build-${{ github.run_number }}
name: "Build #${{ github.run_number }}"
prerelease: true
files: |-
release-apks/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
-23
View File
@@ -1,23 +0,0 @@
<?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>
-8
View File
@@ -1,8 +0,0 @@
<?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>
-10
View File
@@ -1,10 +0,0 @@
<?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>
-17
View File
@@ -1,17 +0,0 @@
<?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>
-12
View File
@@ -45,18 +45,6 @@ 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>
-14
View File
@@ -8,7 +8,6 @@
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Polar Loop Gen 2
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
* Amazfit Neo. But first you have to enable "Discoverable" and "Activity heart rate sharing" in the official Zepp app. (after pairing with OpenTracks you can disable them again)
@@ -113,16 +112,3 @@ Here is a list of the smartphones tested:
| Google | Nexus 4 | LineageOS 18.1 | Ok |
| Samsung | S4 | Android 11 | Ok |
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
## Bosch Smart System eBike
OpenTracks also supports Bosch Smart System eBikes via Bosch's custom BLE service `00000010-eaa2-11e9-81b4-2a2ae2dbcce4`.
Tested with:
* Bosch Perfomance Line CX (SW-Version 16.9.0) / Kiox 300 (SW-Version 16.4.0)
Currently supported measurements:
* Human power
* Cadence
+23 -30
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.4.0'
classpath 'com.android.tools.build:gradle:9.0.0'
}
}
@@ -18,11 +18,11 @@ allprojects {
}
def getVersionName = { ->
return 'git describe --tags'.execute(null, project.rootDir).text.trim()
return 'git describe --tags'.execute().text.trim()
}
def getVersionCodeNightly = { ->
return Integer.valueOf('git rev-list HEAD --count'.execute(null, project.rootDir).text.trim())
return Integer.valueOf('git rev-list HEAD --count'.execute().text.trim())
}
android {
@@ -33,7 +33,7 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
compileSdk = 37
compileSdk = 36
buildFeatures {
viewBinding = true
@@ -46,14 +46,14 @@ android {
}
defaultConfig {
applicationId 'de.dennisguse.opentracks.fork'
versionCode 6751
versionName "v4.28.3"
applicationId 'de.dennisguse.opentracks'
versionCode 6564
versionName "v4.25.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdk = 26
targetSdk = 37
targetSdk = 36
testInstrumentationRunner 'de.dennisguse.opentracks.TestRunner'
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -98,7 +98,6 @@ android {
release {
crunchPngs = false
minifyEnabled = false
signingConfig signingConfigs.debug
}
}
@@ -107,18 +106,20 @@ android {
productFlavors {
nightly {
dimension 'version'
applicationId 'de.dennisguse.opentracks.fork.nightly'
applicationId 'de.dennisguse.opentracks.nightly'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
versionCode getVersionCodeNightly()
versionName getVersionName()
base.archivesName = "de.dennisguse.opentracks.fork.nightly_${getVersionCodeNightly()}"
base.archivesName = "de.dennisguse.opentracks.nightly_${getVersionCodeNightly()}"
signingConfig = signingConfigs.nightly
}
irreproducible {
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
dimension 'version'
applicationId 'de.dennisguse.opentracks.fork'
applicationId 'de.dennisguse.opentracks'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
versionNameSuffix 'irreproducible'
}
reproducible {
@@ -126,9 +127,10 @@ android {
// FDroid: https://f-droid.org/de/packages/de.dennisguse.opentracks.playstore
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
dimension 'version'
applicationId 'de.dennisguse.opentracks.fork.playstore'
applicationId 'de.dennisguse.opentracks.playstore'
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
base.archivesName = "de.dennisguse.opentracks.fork.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
signingConfig = signingConfigs.release
}
@@ -146,33 +148,24 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'androidx.appcompat:appcompat:1.8.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.1.0'
implementation 'com.google.android.material:material:1.14.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.2'
implementation 'androidx.core:core:1.19.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.core:core:1.17.0'
implementation 'androidx.core:core-splashscreen:1.2.0'
implementation 'androidx.mediarouter:mediarouter:1.8.1'
implementation 'androidx.core:core-location-altitude:1.0.0'
implementation 'androidx.work:work-runtime:2.11.2'
implementation 'androidx.health.connect:connect-client:1.1.0'
implementation 'androidx.core:core-location-altitude:1.0.0-beta01'
implementation 'androidx.work:work-runtime:2.11.1'
androidTestImplementation 'androidx.test:core:1.7.0'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test:rules:1.7.0'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'org.mockito:mockito-android:5.21.0'
androidTestUtil 'androidx.test:orchestrator:1.6.1'
}
androidComponents {
onVariants(selector().all()) { variant ->
variant.resValues.put(
new com.android.build.api.variant.impl.ResValueKeyImpl("string", "applicationId"),
new com.android.build.api.variant.ResValue(variant.applicationId.get(), null)
)
}
}
View File
@@ -1,10 +0,0 @@
(versionName): OpenTracks
Ændringer:
-
Fejlrettelser:
-
Udvikler:
-
@@ -1,12 +0,0 @@
v4.26.0: OpenTracks
Changes:
- GPS is optional: configurable in the settings
- BLE sensor address/name is only shown after successful connection
Bugfix:
- Altitude is now shown again in EGM2008 (introduced in v4.25.0)
- Altitude and speed were shown as 0.0 briefly when data was stored
Developer:
- Refactored sensor subsystem
@@ -1,6 +0,0 @@
v4.26.1: OpenTracks
Bugfix:
- Crash when deleting a UI layout
- Crash when heartrate sensor data could not be decoded
- Crash when exporting zero tracks
@@ -1,5 +0,0 @@
v4.26.2: OpenTracks
Bugfix:
- GPS via Bluetooth is not (yet) supported
- Altitude type (EGM2008) wasn't shown
@@ -1,14 +0,0 @@
v4.27.0: OpenTracks
Changes:
- Sensor: power from Bosch eBike
- Sensors: include already bonded Bluetooth devices (like Bosch eBike)
- Barometer: enforce sampling frequency
Bugfix:
- Dashboard API: did not provide update
- GPS could not be enabled with Bluetooth disabled
Developer:
- Removed Mockito
- UI tests: less flacky
@@ -1,11 +0,0 @@
v4.27.1: OpenTracks
Changes:
- Support for Bosch Smart System eBikes (cadence, human power)
- Add activity "gravel biking"
Bugfix:
- While recording: TrackRecordingService would not restart correctly if killed by Android
Developer:
- TargetSDK 37
@@ -1,5 +0,0 @@
v4.27.2: OpenTracks
Bugfix:
- Not all valid timezone offsets could be set
- Fixed race condition for IDLE trackpoints
@@ -1,10 +0,0 @@
v4.28.0: OpenTracks
Changes:
- Support for exporting to Health Connect
Bugfix:
- Dashboard API: when sharing multiple tracks, the currently recorded one wasn't updated
- Configuration setting for announcement of lap power wasn't stored correctly (only some languages affected)
@@ -1,10 +0,0 @@
v4.28.1: OpenTracks
Changes:
- HealthConnect: sending total distance, speed and elevation gain
Bugfix:
- Gravel activity was not selectable
Developer:
- Cleanup ContentProvider URLs
@@ -1,7 +0,0 @@
v4.28.2: OpenTracks
Changes:
- Import: ignore GPX waypoints without time
Bugfix:
- Only start GPS once
@@ -1,10 +0,0 @@
v4.28.3: OpenTracks
Changes:
- Settings: allow to disable segment creation (max recording distance)
- Importer: match activity type case-insensitive
- GPX: export moving speed in summary
Bugfix:
- GPX: importer must respect namespace prefixes
@@ -1,15 +1,15 @@
v3.0.0: OpenTracks
Primera versión de OpenTracks (es decir, una bifurcación dura de MyTracks)
Primer lanzamiento de OpenTracks (es decir, una bifurcación de MyTracks)
Cambios:
- Cambio de nombre de MyTracks a OpenTracks
- 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)
- 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)
@@ -1,17 +0,0 @@
v3.0.1: OpenTracks
Cambios:
- Compartir marcadores
- Configuración reestructurada
- Se ha eliminado el botón «Atrás» de la barra de acciones
- Exportación: se ha eliminado la opción de configuración del tamaño de las fotos
- Exportación: ahora se puede seleccionar el formato KML
Correcciones de errores:
- El directorio de exportación es ahora /OpenTracks/
- El modo horizontal ya no provoca fallos
- Se ha corregido la exportación a KML/KMZ
Desarrollador:
- Las pruebas se pueden ejecutar (aunque muchas fallan)
- Se ha habilitado la integración continua con TravisCI (sin pruebas)
@@ -1,14 +0,0 @@
v3.1.0: OpenTracks
Cambios:
- 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:
- Se solicita permiso de almacenamiento para «Importar todo»/«Exportar todo»
Desarrollador:
- Actualización a Gradle 5.6
- No se minimizan las compilaciones de lanzamiento
@@ -1,16 +0,0 @@
v3.2.0: OpenTracks
Cambios:
- Se ha eliminado el widget
- Se han incorporado iconos de Material Design
- Se han eliminado el snowboard y el esquí (se necesitan nuevos iconos)
Corrección de errores:
- Se ha corregido la funcionalidad de búsqueda
Desarrollador:
- Se han corregido (algunos) casos de prueba
- Se ha eliminado la funcionalidad «parcelable» en desuso
- Limpieza y reformateo a fondo
- No se utiliza la difusión para la comunicación entre la aplicación y el widget
- El archivo stats_content.xml no incluye otros archivos
@@ -1,7 +0,0 @@
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)
@@ -1,13 +0,0 @@
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)
@@ -1,14 +0,0 @@
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
@@ -1,5 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -1,10 +0,0 @@
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)
@@ -1,10 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,11 +0,0 @@
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)
@@ -1,12 +0,0 @@
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
@@ -1,13 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,12 +0,0 @@
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)
@@ -1,14 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,4 +0,0 @@
v3.6.1: OpenTracks
Corrección de errores:
- Importación de KMZ: guardar la ruta correcta de la imagen
@@ -1,4 +0,0 @@
v3.6.2: OpenTracks
Corrección de errores:
- Los gráficos no se mostraban correctamente (problema introducido en la v3.6.0)
@@ -1,13 +0,0 @@
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
@@ -1,7 +0,0 @@
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
@@ -1,6 +0,0 @@
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
@@ -1,7 +0,0 @@
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
@@ -1,12 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,11 +0,0 @@
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)
@@ -1,16 +0,0 @@
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
@@ -1,8 +0,0 @@
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
@@ -1,14 +0,0 @@
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:
- Se puede desactivar la detección de inactividad
- Detección de inactividad se puede desactivar
Corrección de errores:
- La interfaz de usuario para añadir imágenes a los marcadores estaba oculta
Correcciones de errores:
- UI para añadir imágenes a los marcadores estaba oculto
@@ -1,14 +0,0 @@
v4.27.0: OpenTracks
Cambios:
- Sensor: potencia de la bicicleta eléctrica Bosch
- Sensores: se incluyen los dispositivos Bluetooth ya emparejados (como la bicicleta eléctrica Bosch)
- Barómetro: se aplica la frecuencia de muestreo
Corrección de errores:
- API del panel de control: no se proporcionaba la actualización
- No se podía activar el GPS con el Bluetooth desactivado
Desarrollador:
- Se ha eliminado Mockito
- Pruebas de la interfaz de usuario: menos inestables
@@ -1,22 +1,8 @@
Un compañero de seguimiento deportivo que respeta tu privacidad.
OpenTracks es un tracker deportivo que respeta tu privacidad.
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
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
@@ -1,5 +0,0 @@
v4.26.2: OpenTracks
Arranxos:
- (aínda) non hai compatibilidade co GPS vía Bluetooth
- non se mostraba a altitude tipo (EGM2008)
@@ -1,15 +0,0 @@
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)
@@ -1,17 +0,0 @@
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)
@@ -1,14 +0,0 @@
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 @@
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
Teman pelacakan olahraga yang 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 ditampilkan dalam EGM2008 (di atas permukaan laut); diekspor sebagai WGS84.
* Ketinggian yang 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 / Sumber Terbuka
Perangkat lunak bebas / Perangkat Lunak Bebas / Sumber Terbuka
yang berarti Anda dapat menggunakan, mempelajari, mengubah, dan berbagi kode sumber.
Berlisensi Apache 2.0
@@ -1 +1 @@
Sebuah teman pelacak olahraga yang benar-benar menghormati privasi Anda.
Sebuah pelacak olahraga yang benar-benar menghormati privasi Anda.
@@ -1,5 +0,0 @@
v4.26.2: OpenTracks
Correzioni:
- Il GPS via Bluetooth non è (ancora) supportato
- Il tipo di altitudine (EGM2008) non viene mostrato
@@ -1,22 +0,0 @@
Un tracker sportivo che rispetta la tua privacy.
Sport e attività all'aria aperta portano a felicità, allenamento e fiducia
Valorizza la tua salute tenendo traccia dei tuoi progressi.
Registra quando stai correndo o passeggiando e regalati un monitor più grande quando fai ciclismo.
Segna con fotografie i posti interessanti incrociati durante il tragitto.
Tieni traccia delle statistiche in dettaglio per analizzarle.
Condividi solo i dati che vuoi.
* Annunci vocali.
* Supporto a sensori Bluetooth LE: battito cardiaco, velocità e distanza (ciclismo), cadenza (ciclismo), rilevamento potenza (ciclismo).
* Altitudine positiva o negativa tramite sensore barometrico.
* Altitudine mostrata in EGM2008 (sopra il livello del mare) ed esportata come WGS84.
* Esporta tracce come KMZ (con foto incluse), KML o GPX.
* Non richiede accesso a internet o altri permessi.
* Temi chiaro e scuro, o seguendo la impostazioni di sistema.
* Nessuna pubblicità.
Libre software / Free Software / Open Source
cioè puoi usare, studiare, cambiare e condividere il codice sorgente.
Licenza: Apache 2.0
@@ -1,22 +1,11 @@
Um companheiro de rastreamento esportivo que respeita sua privacidade.
OpenTracks é um aplicativo de rastreamento de esportes que respeita totalmente a sua privacidade.
Esportes e atividades ao ar livre garantem felicidade, habilidades e confiança.
Valoriza sua saúde acompanhando os seus treinos.
Grava enquanto você corre ou caminha e te entrega um computador de bicicleta com uma tela maior para o ciclismo.
Marca locais interessantes durante o percurso com fotos.
Deixa estatísticas registradas em grande detalhe para analises.
Compartilha apenas os dados que você quer que outros tenham.
* Anúncios por voz.
* Suporte para sensores Bluetooth LE: frequência cardíaca, velocidade e distância (ciclismo), cadência (ciclismo) e medição de potência (ciclismo).
* Ganhos e perdas de altitude: via sensor barométrico.
* Altitudes exibidas em EGM2008 (acima do nível médio do mar); exportado como WGS84.
* Exporta os dados de rastreio como KMZ (inclusive fotos), KML ou GPX.
* Sem acesso à internet ou permissões extra.
* Tema claro e escuro, respeitando as configurações do sistema.
* Sem propagandas.
Software Livre / Código Aberto
Significa que você pode usar, estudar, mudar e compartilhar o código.
Licença Apache 2.0
Características:
* Rastreamento de esportes: monitore seu esporte e atividades ao ar livre
* Fotos e marcadores: marque locais interessantes durante o rastreamento
* Ganho e perda de altitude: via sensor barométrico
* Altitude mostrada no EGM2008 (acima do nível médio do mar); exportado como WGS84
* Anúncios de voz
* Suporta sensores Bluetooth LE: frequência cardíaca, velocidade e distância (ciclismo), cadência (ciclismo) e medidor de potência (ciclismo)
* Exportar dados: exporte trilhas como KMZ (incluindo fotos), KML, ou GPX
* Sem acesso à Internet: a Internet não é usada
@@ -1 +1 @@
一款尊重您隱私的運動蹤跡紀錄器。
一個尊重您的隱私的運動蹤跡紀錄器。
-13
View File
@@ -1,13 +0,0 @@
#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
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
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
-13
View File
@@ -1,13 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
cat src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java | grep -n "if (movingDistance != null) {" -B 2 -A 30
-14
View File
@@ -1,14 +0,0 @@
<html>
<body>
<h1>OpenTracks: Privacy Policy</h1>
<p>
OpenTracks does only store data on the local device that is relevant for tracking your sport exercise.
Stored data is not transmitted from the app itself to a third party.
Data is stored by OpenTracks until:
1. the user deletes the recorded track or
2. the user uninstalls OpenTracks
</p>
</body>
</html>
-4
View File
@@ -1,4 +0,0 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'OpenTracks'
@@ -2,117 +2,76 @@ package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.EspressoUtils.withRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.renameStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@Test
public void deleteTrackFromTrackStoppedActivity() {
recordTrackAndOpenTrackStoppedActivity();
public void espressoDeleteTrackTest() {
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
// TrackStoppedActivity: discard the just-recorded track directly
onView(withId(R.id.discard_button))
.perform(performClick());
onView(withText(android.R.string.ok))
.inRoot(isDialog())
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
// the recycler is empty again after discarding the only recorded track
onView(withId(R.id.track_list))
.check(matches(withRecyclerViewItemCount(0)));
}
@Test
public void deleteTrackFromSelectedTrackInTrackList() {
String uniqueTrackName = "DeleteFromTrackList-" + System.currentTimeMillis();
recordTrackAndOpenTrackStoppedActivity();
renameStoppedTrack(uniqueTrackName, UI_TIMEOUT);
// TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action))
// wait; stay recording
.perform(waitFor(5000))
// stop;
.perform(longClick());
// TrackStoppedActivity
onView(withId(R.id.finish_button))
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 1, UI_TIMEOUT);
.perform(click());
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
.perform(performLongClickOnAncestor());
// select track
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0)))
.perform(longClick());
clickDeleteSelectedTrackAction();
onView(withText(android.R.string.ok))
.inRoot(isDialog())
.perform(performClick());
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
// open menu and delete selected track
onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(androidx.appcompat.R.id.action_mode_bar),
1),
2),
isDisplayed()))
.perform(click());
onView(withId(R.id.track_list))
.check(matches(withRecyclerViewItemCount(0)));
onView(withText("Delete"))
.perform(click());
onView(withText("OK"))
.perform(click());
// tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
}
private void recordTrackAndOpenTrackStoppedActivity() {
startRecordingFromTrackList(UI_TIMEOUT);
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.discard_button, R.id.finish_button);
}
private void clickDeleteSelectedTrackAction() {
try {
waitUntilDisplayed(withId(R.id.list_context_menu_delete), UI_TIMEOUT);
onView(withId(R.id.list_context_menu_delete))
.perform(performClick());
} catch (AssertionError ignored) {
onView(allOf(
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
isDescendantOfA(withId(androidx.appcompat.R.id.action_mode_bar))))
.perform(performClick());
onView(withText(R.string.menu_delete))
.inRoot(isPlatformPopup())
.perform(performClickOnAncestor());
}
}
}
@@ -2,43 +2,34 @@ package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.Locale;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoEditTrackRecordingTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
@Rule
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
@@ -48,27 +39,27 @@ public class EspressoEditTrackRecordingTest {
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
@LargeTest
@Test
public void espressoEditTrackRecordingTest() {
startRecordingFromTrackList(UI_TIMEOUT);
{
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity
// wait; stay recording
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(4000));
// open menu
onView(allOf(
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
isDescendantOfA(withId(R.id.bottom_app_bar))))
.perform(performClick());
openContextualActionModeOverflowMenu();
// Click the item.
onView(withText(R.string.menu_edit))
.inRoot(isPlatformPopup())
.perform(performClickOnAncestor());
waitUntilDisplayed(withId(R.id.track_edit_name), UI_TIMEOUT);
.perform(click());
// change name for "New Name"
onView(withId(R.id.track_edit_name))
@@ -79,11 +70,11 @@ public class EspressoEditTrackRecordingTest {
// save edition
onView(withId(R.id.track_edit_save))
.perform(performClick());
waitUntilDisplayed(withId(R.id.track_recording_fab_action), UI_TIMEOUT);
.perform(click());
// stop;
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
onView(withId(R.id.track_recording_fab_action))
.perform(longClick());
// it's on track stopped activity and there are two buttons
onView(withId(R.id.resume_button))
@@ -1,141 +1,100 @@
package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.withSelectedTab;
import static de.dennisguse.opentracks.util.EspressoUtils.withViewPagerCurrentItem;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.finishStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.resumeStoppedTrack;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.data.models.TrackPoint;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoUITest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
private final ActivityScenarioRule<TrackListActivity> activityRule = new ActivityScenarioRule<>(TrackListActivity.class);
private final GrantPermissionRule grantPermissionRule = TestUtil.createGrantPermissionRule();
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public RuleChain rules = RuleChain.outerRule(grantPermissionRule)
.around(activityRule);
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
}
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
@LargeTest
@Test
public void record_stop_resume_stop_finish() {
startRecordingFromTrackList(UI_TIMEOUT);
{
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button);
resumeStoppedTrack(UI_TIMEOUT);
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.finish_button);
finishStoppedTrack(UI_TIMEOUT);
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity: wait to record some time and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
onView(withId(R.id.track_list))
.check(matches(isDisplayed()));
// TrackStoppedActivity: resume
onView(allOf(withId(R.id.resume_button), isClickable()))
.perform(click());
// TrackRecordingActivity: wait and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
// TrackStoppedActivity
onView(withId(R.id.finish_button))
.perform(click());
}
}
@LargeTest
@Test
public void record_move_through_tabs() {
startRecordingFromTrackList(UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.track_detail_activity_tablayout), UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.track_detail_activity_view_pager), UI_TIMEOUT);
{
// TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action))
.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
tabLayout.perform(selectTabAtIndex(1));
waitUntilTabSelectionSettlesAt(1);
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(2));
waitUntilTabSelectionSettlesAt(2);
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(3));
waitUntilTabSelectionSettlesAt(3);
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(0));
waitUntilTabSelectionSettlesAt(0);
tabLayout.perform(waitFor(1000));
// stop
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
onView(withId(R.id.resume_button))
.check(matches(isDisplayed()));
onView(withId(R.id.finish_button))
.check(matches(isDisplayed()));
onView(withId(R.id.track_recording_fab_action)).
perform(longClick());
}
}
@LargeTest
@Test
public void selectAndDeleteTrack() {
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 5);
String uniqueTrackName = "SelectAndDeleteTrack-" + System.currentTimeMillis();
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
trackBuilder.setName(uniqueTrackName);
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
activityRule.getScenario().recreate();
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
.perform(performLongClickOnAncestor());
onView(withId(androidx.appcompat.R.id.action_mode_bar))
.check(matches(isDisplayed()));
}
private void waitUntilTabSelectionSettlesAt(int index) {
waitForResumedViewCondition(
withId(R.id.track_detail_activity_tablayout),
view -> withSelectedTab(index).matches(view),
"selected tab to be " + index,
UI_TIMEOUT);
waitForResumedViewCondition(
withId(R.id.track_detail_activity_view_pager),
view -> withViewPagerCurrentItem(index).matches(view),
"ViewPager current item to be " + index,
UI_TIMEOUT);
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
}
}
@@ -20,19 +20,22 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
@@ -75,6 +78,7 @@ import de.dennisguse.opentracks.util.FileUtils;
* @author Bartlomiej Niechwiej
* @author Youtao Liu
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
@@ -86,6 +90,12 @@ public class CustomContentProviderUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Mock
private Cursor cursorMock;
@Mock
private ContentResolver contentResolverMock;
@Before
public void setUp() {
contentProviderUtils = new ContentProviderUtils(context);
@@ -94,21 +104,21 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
assertEquals(0, testIterator(new Track.Id(1), 0).size());
testIterator(new Track.Id(1), 0);
}
@Test
public void testLocationIterator_noAscending() {
assertEquals(50, testIterator(new Track.Id(1), 50).size());
assertEquals(50, testIterator(new Track.Id(2), 50).size());
testIterator(new Track.Id(1), 50);
testIterator(new Track.Id(2), 50);
}
@Test
public void testLocationIterator_largeTrack() {
assertEquals(20000 / 2, testIterator(new Track.Id(1), 20000 / 2).size());
testIterator(new Track.Id(1), 20000 / 2);
}
private List<TrackPoint> testIterator(Track.Id trackId, int numPoints) {
private void testIterator(Track.Id trackId, int numPoints) {
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(trackId, null)) {
@@ -121,7 +131,6 @@ public class CustomContentProviderUtilsTest {
}
assertEquals(numPoints, locations.size());
}
return locations;
}
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
@@ -178,62 +187,40 @@ public class CustomContentProviderUtilsTest {
@Test
public void testCreateTrack() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
int columnIndex = 1;
// Id
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getLong(columnIndex)).thenReturn(trackId.id());
//Uuid
columnIndex++;
when(cursorMock.getColumnIndexOrThrow(TracksColumns.UUID)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getBlob(columnIndex)).thenReturn(UUIDUtils.toBytes(UUID.randomUUID()));
// Name
columnIndex++;
String name = NAME_PREFIX + trackId.id();
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex);
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getString(columnIndex)).thenReturn(name);
MatrixCursor cursor = new MatrixCursor(new String[]{
TracksColumns._ID,
TracksColumns.UUID,
TracksColumns.NAME,
TracksColumns.DESCRIPTION,
TracksColumns.ACTIVITY_TYPE,
TracksColumns.ACTIVITY_TYPE_LOCALIZED,
TracksColumns.STARTTIME,
TracksColumns.STARTTIME_OFFSET,
TracksColumns.STOPTIME,
TracksColumns.TOTALDISTANCE,
TracksColumns.TOTALTIME,
TracksColumns.MOVINGTIME,
TracksColumns.MAXSPEED,
TracksColumns.MIN_ALTITUDE,
TracksColumns.MAX_ALTITUDE,
TracksColumns.ALTITUDE_GAIN,
TracksColumns.ALTITUDE_LOSS,
});
cursor.addRow(new Object[]{
trackId.id(),
UUIDUtils.toBytes(UUID.randomUUID()),
name,
"",
ActivityType.UNKNOWN.getId(),
"",
1_000L,
ZoneOffset.UTC.getTotalSeconds(),
2_000L,
123.45f,
1_000L,
500L,
12.34f,
null,
null,
null,
null,
});
assertTrue(cursor.moveToFirst());
Track track = ContentProviderUtils.createTrack(cursor);
Track track = ContentProviderUtils.createTrack(cursorMock);
assertEquals(trackId, track.id());
assertEquals(name, track.name());
}
private void assertCount(int trackCount, int trackPointCount, int markerCount) {
ContentResolver contentResolver = context.getContentResolver();
try (Cursor tracksCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKS.getUri(), null, null, null, TracksColumns._ID)) {
try (Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID)) {
assertEquals(trackCount, tracksCursor.getCount());
}
try (Cursor tracksPointsCursor = contentResolver.query(CustomContentProvider.UrlType.TRACKPOINTS_BY_ID.getUri(), null, null, null, TrackPointsColumns._ID)) {
try (Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID)) {
assertEquals(trackPointCount, tracksPointsCursor.getCount());
}
try (Cursor markerCursor = contentResolver.query(CustomContentProvider.UrlType.MARKERS.getUri(), null, null, null, MarkerColumns._ID)) {
try (Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID)) {
assertEquals(markerCount, markerCursor.getCount());
}
}
@@ -462,6 +449,8 @@ public class CustomContentProviderUtilsTest {
marker.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(marker.getMarker());
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
marker.setId(markerId);
ContentValues contentValues = contentProviderUtils.createContentValues(marker.getMarker(), trackId);
@@ -1195,7 +1184,7 @@ public class CustomContentProviderUtilsTest {
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0f);
}
private Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
// given
Instant start = Instant.now();
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
@@ -1218,27 +1207,22 @@ public class CustomContentProviderUtilsTest {
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
return Pair.create(sensorStatistics, stats);
// then
assertEquals(sensorStatistics.avgHeartRate().getBPM(), stats.avgHr, 0.01f);
assertEquals(sensorStatistics.maxHeartRate().getBPM(), stats.maxHr, 0.01f);
assertEquals(sensorStatistics.avgCadence().getRPM(), stats.avgCadence, 0.01f);
assertEquals(sensorStatistics.maxCadence().getRPM(), stats.maxCadence, 0.01f);
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0.01f);
}
@Test
public void testGetSensorStats_veryLongActivity12h() {
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(43200 / 6, false);
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
testGetSensorStats_randomData(43200 / 6, false);
}
@Test
public void testGetSensorStats_withSeveralRandomStartSegments() {
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(5000, true);
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
testGetSensorStats_randomData(5000, true);
}
private TrackPoint getLastValidTrackPoint(Track.Id trackId) {
@@ -1,78 +0,0 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ActivityTypeTest {
@Test
public void findBy_id() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running"));
}
@Test
public void findBy_idOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
assertEquals(activityType, ActivityType.findBy(activityType.getId()));
}
}
@Test
public void findBy_synonymsOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
for (String synonym : activityType.synonyms) {
assertEquals(activityType, ActivityType.findBy(synonym));
assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_')));
}
}
}
@Test
public void findBy_ignoresCase() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING"));
assertEquals(ActivityType.ATV, ActivityType.findBy("atv"));
}
@Test
public void findBy_ignoresSeparators() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking"));
assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing"));
}
@Test
public void findBy_ignoresSurroundingAndRepeatedWhitespace() {
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking "));
}
@Test
public void findBy_synonym() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill"));
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE"));
assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER"));
assertEquals(ActivityType.BIKING, ActivityType.findBy("bike"));
}
@Test
public void findBy_unknown() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft"));
}
@Test
public void findBy_null() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null));
}
@Test
public void findBy_blank() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" "));
}
}
@@ -89,12 +89,12 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
public class ExportImportTest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(50, TimeUnit.SECONDS); //TODO 5s
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
//For csv_export_only() as the timezone is hardcoded in the expectation.
//For csv_export_only() as we the timezone is hardcoded in the expectation.
@Rule
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
@@ -199,6 +199,7 @@ public class ExportImportTest {
));
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.getSensorManager().clear();
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -213,6 +214,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
trackPointCreator.getSensorManager().clear();
trackPointCreator.setClock("2020-02-02T02:04:00Z");
service.endCurrentTrack();
@@ -296,22 +298,7 @@ public class ExportImportTest {
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
new Position(
Instant.parse("2020-02-02T02:02:18Z"),
3.123456, 14.001456, Distance.of(10),
Altitude.WGS84.of(1020.25), null,
null,
Speed.of(5)),
Distance.of(0),
HeartRate.of(69),
null,
Cadence.of(3),
Power.of(50),
new AltitudeGainLoss(0, 0)
),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
new TrackPoint(
null,
@@ -369,16 +356,7 @@ public class ExportImportTest {
null,
null,
new AltitudeGainLoss(0, 0)),
new TrackPoint(
null,
TrackPoint.Type.SEGMENT_END_MANUAL,
Position.of(Instant.parse("2020-02-02T02:04:00Z")),
null,
null,
null,
null,
null,
new AltitudeGainLoss(0, 0))
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
), actual);
assertEquals(new Statistics(
@@ -769,7 +747,7 @@ public class ExportImportTest {
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
}
public static void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
Location location = new Location("mock");
location.setLatitude(latitude);
location.setLongitude(longitude);
@@ -120,184 +120,6 @@ public class GPXTrackImporterTest {
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_missing_time() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_missing_time);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("20210907_213924.gpx", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackstatistics
Statistics trackStatistics = importedTrack.statistics();
assertEquals(0, trackStatistics.maxSpeed().toMPS(), 0.01);
assertEquals(Duration.ofSeconds(0), trackStatistics.movingDuration());
// 4. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(1, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null))
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_pwd() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_pwr);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_gpxpx() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_gpxpx);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_speed_no_namespace() throws IOException {
@@ -463,8 +285,8 @@ public class GPXTrackImporterTest {
public void importExportTest_timezone() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_timezone)) {
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
try (InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
@@ -26,7 +26,6 @@ public class TrackPointAssert {
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
if (expected.position().hasAltitude()) {
Assert.assertEquals("altitude", expected.position().altitude().getClass(), actual.position().altitude().getClass());
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
}
@@ -14,7 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.data.CustomContentProvider;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@RunWith(AndroidJUnit4.class)
public class DataProviderTest {
@@ -24,7 +24,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_tracks() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = new String[]{
"_id",
"name",
@@ -64,7 +64,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_trackpoints() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
@@ -94,7 +94,7 @@ public class DataProviderTest {
@Test
public void supportsVersion2_markers() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
@@ -126,7 +126,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_tracks() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = {
"_id",
"name",
@@ -164,7 +164,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_trackpoints() {
//given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
@@ -194,7 +194,7 @@ public class DataProviderTest {
@Test
public void supportsVersion3_markers() {
// given
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
@@ -1,137 +0,0 @@
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
public class BoschEbikeParserTest {
@Test
public void parse_multipleFrames_returnsCombinedMeasurements() {
byte[] raw = new byte[] {
// Bosch cadence frame
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
// Bosch human power frame
0x30, 0x05, (byte) 0x98, 0x5B, 0x08, (byte) 0x83, 0x01,
// Bosch speed frame
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0x94, 0x0B, 0x10, 0x01
};
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
assertEquals(131.0f, data.humanPower().getW(), 0.01f);
assertEquals(14.28 / 3.6, data.speed().toMPS(), 0.0001);
}
@Test
public void parse_multiByteVarint_returnsScaledSpeed() {
// Bosch speed frame using a multi-byte varint
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0xE1, 0x0E, 0x10, 0x01
});
assertNotNull(data);
assertEquals(18.89 / 3.6, data.speed().toMPS(), 0.0001);
}
@Test
public void parse_zeroValueFrame_returnsZeroCadence() {
// Bosch zero-value cadence frame
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x02, (byte) 0x98, 0x5A
});
assertNotNull(data);
assertEquals(0.0f, data.cadence().getRPM(), 0.01f);
assertNull(data.speed());
assertNull(data.humanPower());
}
@Test
public void parse_unsupportedMeasurementsOnly_returnsNull() {
// Bosch frame for message 0x9865, which the parser does not map to a measurement.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x02, (byte) 0x98, 0x65
});
assertNull(data);
}
@Test
public void parse_unimplementedMeasurementsOnly_returnsNull() {
// Bosch motor power frame: message 0x985D with value 0x42, currently ignored.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x04, (byte) 0x98, 0x5D, 0x08, 0x42
});
assertNull(data);
}
@Test
public void parse_malformedVarint_returnsNull() {
// Truncated Bosch speed frame: starts like a real 0x982D frame but the varint is incomplete.
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
});
assertNull(data);
}
@Test
public void parse_varintLargerThan32Bit_returnsNull() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
0x30, 0x08, (byte) 0x98, 0x2D, 0x08,
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01
});
assertNull(data);
}
@Test
public void parse_validMeasurementBeforeMalformedFrame_returnsValidMeasurement() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
// Truncated Bosch speed frame: malformed trailing frame should not discard the valid cadence above.
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
});
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
assertNull(data.speed());
}
@Test
public void parse_malformedVarintDoesNotConsumeNextFrameStartByte() {
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
// Truncated Bosch speed frame: the varint continuation bit is set, but the frame ends
// before the next varint byte. The following frame start byte must not be consumed.
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94,
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
});
assertNotNull(data);
assertNull(data.speed());
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
}
@Test
public void parse_leadingNoise_ignored() {
byte[] raw = new byte[] {
// Noise bytes to verify the parser resynchronizes on the Bosch frame start byte 0x30.
0x01, 0x02, 0x03,
// Bosch cadence frame: message 0x985A with value 0x76
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
};
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
assertNotNull(data);
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
}
}
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.sensors.driver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import android.bluetooth.BluetoothGattCharacteristic;
@@ -36,27 +35,4 @@ public class CyclingPowerBluetoothTest {
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
}
@Test
public void parsePayload_boschPower() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
// Bosch human power frame: message 0x985B with value 0x7B => 123 W.
characteristic.setValue(new byte[] { 0x30, 0x04, (byte) 0x98, 0x5B, 0x08, 0x7B });
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
assertEquals(123, power.power().getW(), 0.01);
assertNull(power.crank());
}
@Test
public void parsePayload_boschCadenceOnly_returnsNull() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
// Bosch cadence-only frame: message 0x985A with value 0x01B8 => 220 / 2 = 110 rpm, so power parsing should ignore it.
characteristic.setValue(new byte[] { 0x30, 0x05, (byte) 0x98, 0x5A, 0x08, (byte) 0xB8, 0x01 });
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
assertNull(power);
}
}
}
@@ -96,25 +96,6 @@ public class SensorDataCyclingTest {
assertNull(current.getAggregatedValue(Instant.MIN));
}
@Test
public void compute_cadence_directCadence_withoutPrevious() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test
public void compute_cadence_directCadence_winsOverCrankData() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1L, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test
public void compute_speed() {
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
@@ -1,24 +1,24 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
@@ -29,24 +29,32 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.UnitSystem;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private static final String CHANNEL_ID = "TrackRecordingServiceNotificationManagerTest";
private final Context context = ApplicationProvider.getApplicationContext();
private final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
@After
public void tearDown() {
notificationManager.cancelAll();
}
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@Mock
private NotificationManager notificationManager;
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
TrackPoint trackPointMock = new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
null,
null, null, Distance.of(999f),
Altitude.WGS84.of(10), null,
null,
Speed.ZERO)
);
TrackingNotificationBuilder notificationBuilder = new TrackingNotificationBuilder(context, CHANNEL_ID);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationBuilder);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
.thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
subject.setUnitSystem(UnitSystem.METRIC);
Track track = new Track(
@@ -60,43 +68,16 @@ public class TrackRecordingServiceNotificationManagerTest {
Statistics.DEFAULT
);
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(1000));
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
assertEquals(java.util.Arrays.asList(false, true, true, true, true, true, false, true), notificationBuilder.onlyAlertOnceValues);
RecordingData data = new RecordingData(track, trackPointMock, null, null);
// when
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(100));
subject.updateTrackPoint(context, data, Distance.of(1000));
subject.updateTrackPoint(context, data, Distance.of(100));
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
}
private RecordingData createRecordingData(Track track, Distance horizontalAccuracy) {
TrackPoint trackPoint = new TrackPoint(
TrackPoint.Type.TRACKPOINT,
new Position(
null,
null,
null,
horizontalAccuracy,
Altitude.WGS84.of(10),
null,
null,
Speed.ZERO
)
);
return new RecordingData(track, trackPoint, null, null);
}
private static final class TrackingNotificationBuilder extends NotificationCompat.Builder {
private final List<Boolean> onlyAlertOnceValues = new ArrayList<>();
private TrackingNotificationBuilder(Context context, String channelId) {
super(context, channelId);
setSmallIcon(R.drawable.ic_logo_color_24dp);
}
@NonNull
@Override
public NotificationCompat.Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
onlyAlertOnceValues.add(onlyAlertOnce);
return super.setOnlyAlertOnce(onlyAlertOnce);
}
}
}
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.Intent;
@@ -94,41 +93,6 @@ public class TrackRecordingServiceRecordingTest {
service = startService();
}
@MediumTest
@Test
public void reCreate_not_recording() {
// given
TrackIdStorage.unset(context);
assertFalse(service.isRecording());
// when
service.onStartCommand(null, 0, 1);
// then
assertFalse(service.isRecording());
}
@MediumTest
@Test
public void reCreate_recording() {
// given
assertFalse(service.isRecording());
Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording());
// when (crash and restart)
service.onDestroy();
service.onCreate();
service.onStartCommand(null, 0, 1);
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingStatusObservable().getValue().trackId());
service.endCurrentTrack();
}
@MediumTest
@Test
public void recording_startStop() {
@@ -15,7 +15,6 @@
*/
package de.dennisguse.opentracks.services;
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -42,7 +41,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -58,8 +56,6 @@ import de.dennisguse.opentracks.sensors.GpsStatusValue;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceStateMachineTest {
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(2);
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@@ -127,7 +123,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.tryStartSensors();
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -136,12 +131,11 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.stopSensors();
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
assertEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording.
}
@MediumTest
@@ -152,9 +146,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
Track.Id trackId = service.startNewTrack();
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilRecordingDataIsNotDefault();
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -163,13 +154,9 @@ public class TrackRecordingServiceStateMachineTest {
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
service.endCurrentTrack();
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// when
service.resumeTrack(trackId);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -180,8 +167,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.endCurrentTrack();
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
// then
assertFalse(service.isRecording());
@@ -192,8 +177,6 @@ public class TrackRecordingServiceStateMachineTest {
// when
service.resumeTrack(trackId);
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
// then
assertTrue(service.isRecording());
@@ -271,25 +254,4 @@ public class TrackRecordingServiceStateMachineTest {
assertNotNull(trackId);
assertNull(newTrackId);
}
private void waitUntilRecordingDataIsNotDefault() {
waitForValue(service.getRecordingDataObservable(),
recordingData -> !RecordingData.NOT_RECORDING.equals(recordingData),
WAIT_TIMEOUT,
"recording data to leave NOT_RECORDING");
}
private void waitUntilGpsStatusIs(GpsStatusValue expected) {
waitForValue(service.getGpsStatusObservable(),
expected::equals,
WAIT_TIMEOUT,
"gps status to become " + expected);
}
private void waitUntilRecordingStatusIs(RecordingStatus expected) {
waitForValue(service.getRecordingStatusObservable(),
expected::equals,
WAIT_TIMEOUT,
"recording status to become " + expected);
}
}
@@ -1,121 +0,0 @@
package de.dennisguse.opentracks.services;
import static org.junit.Assert.assertEquals;
import static de.dennisguse.opentracks.services.TrackRecordingService.RECORDING_DATA_UPDATE_INTERVAL;
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
import android.content.Context;
import android.content.Intent;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.GpsHandler;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceUITest {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private static final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@Test
public void getDataForUI_gps() throws TimeoutException {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
RecordingData recordingData;
// given
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock("2020-02-02T02:02:02Z");
// when
service.startNewTrack();
disconnectRealGps(trackPointCreator.getSensorManager().getGpsHandler());
trackPointCreator.getSensorManager().reset();
RecordingData initialRecordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:02Z"), false);
// then
TrackPoint initialTrackPoint = initialRecordingData.latestTrackPoint();
assertEquals(TrackPoint.Type.TRACKPOINT, initialTrackPoint.type());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), initialTrackPoint.getTime());
// when
// In this step, the data is saved (and Aggregator.resetAggregated called), but data should still be shown in the UI.
trackPointCreator.setClock("2020-02-02T02:02:03Z");
ExportImportTest.sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
recordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:03Z"), true);
// then
new TrackPointAssert().assertEquals(new TrackPoint(
null,
TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3.1234567, 14.0014567, null,
Altitude.EGM2008.of(1013.05), null,
null,
Speed.of(15)),
null,
null,
null,
null,
null,
new AltitudeGainLoss(1, 1)
),
recordingData.latestTrackPoint());
}
private void disconnectRealGps(GpsHandler gpsHandler) {
if (gpsHandler.driver != null) {
gpsHandler.driver.disconnect();
}
gpsHandler.onConnected(null, GpsInternal.LOCATION_PROVIDER);
}
private RecordingData waitUntilLatestTrackPointMatches(TrackRecordingService service, Instant expectedTime, boolean requiresLocation) {
return waitForValue(service.getRecordingDataObservable(),
recordingData -> {
TrackPoint latestTrackPoint = recordingData != null ? recordingData.latestTrackPoint() : null;
return latestTrackPoint != null
&& expectedTime.equals(latestTrackPoint.getTime())
&& (!requiresLocation || latestTrackPoint.position().hasLocation());
},
RECORDING_DATA_UPDATE_INTERVAL.multipliedBy(3),
"latest track point to reach " + expectedTime + (requiresLocation ? " with location" : ""));
}
}
@@ -52,7 +52,6 @@ public class VoiceAnnouncementUtilsTest {
@Before
public void setUp() {
PreferencesUtils.setVoiceAnnounceUnit(true);
PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
@@ -92,10 +91,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -135,10 +131,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -178,10 +171,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -221,10 +211,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -234,51 +221,6 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("12:00 AM. Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement);
}
@Test
public void getAnnouncement_metric_distance_without_unit() {
PreferencesUtils.setVoiceAnnounceUnit(false);
Track track = new Track(
null,
null,
"",
"",
"",
ActivityType.AIRPLANE,
ZoneOffset.UTC,
new Statistics(
Instant.EPOCH,
Instant.EPOCH,
Duration.ofHours(2).plusMinutes(5).plusSeconds(10),
Duration.ofHours(1),
Distance.of(19900),
Speed.of(100),
null,
new AltitudeGainLoss(6000, 6000),
null,
null
));
SensorDataSet dataSet = new SensorDataSet(
null,
null,
null,
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString();
// then
assertEquals("12:00 AM. Total distance 19.9. 1 hour. Average moving speed 19.9.", announcement);
}
@Test
public void getAnnouncement_withInterval_metric_speed() {
// given
@@ -291,10 +233,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -334,10 +273,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -359,10 +295,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -402,10 +335,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -445,10 +375,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -488,10 +415,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -531,10 +455,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -556,10 +477,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -599,10 +517,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -624,10 +539,7 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
@@ -644,7 +556,6 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
@@ -655,17 +566,14 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
}
@Test
@@ -681,7 +589,6 @@ public class VoiceAnnouncementUtilsTest {
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
@@ -692,17 +599,14 @@ public class VoiceAnnouncementUtilsTest {
new SensorData<>(HeartRate.of(60), "unused"),
null, null,
null,
null,
null,
null,
null
null
);
// when
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
// then
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
}
@Test
@@ -1,51 +0,0 @@
package de.dennisguse.opentracks.settings;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
@RunWith(AndroidJUnit4.class)
public class ImportExportSettingsIntentTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void applicationIdResource_matchesInstalledPackage() {
assertEquals(BuildConfig.APPLICATION_ID, context.getString(R.string.applicationId));
assertEquals(context.getPackageName(), context.getString(R.string.applicationId));
}
@Test
public void importExportPreferenceTargets_areResolvableInCurrentPackage() throws PackageManager.NameNotFoundException {
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity");
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity");
}
private void assertResolvable(String activityClassName) throws PackageManager.NameNotFoundException {
String applicationId = context.getString(R.string.applicationId);
ComponentName componentName = new ComponentName(applicationId, activityClassName);
ActivityInfo activityInfo = context.getPackageManager().getActivityInfo(componentName, 0);
assertEquals(applicationId, activityInfo.packageName);
ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(new Intent().setComponent(componentName), 0);
assertNotNull(resolveInfo);
}
}
@@ -1,41 +1,36 @@
package de.dennisguse.opentracks.ui.aggregatedStatistics;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.stopTrackRecordingService;
import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.util.Pair;
import android.view.View;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TestUtil;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
@@ -46,69 +41,53 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(AndroidJUnit4.class)
public class EspressoAggregatedFilterTest {
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
private ActivityScenario<AggregatedStatisticsActivity> scenario;
@Before
public void setUp() {
resetTrackRecordingServiceAndDeleteTracks();
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(androidx.test.core.app.ApplicationProvider.getApplicationContext());
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
trackBuilder.setActivityTypeLocalized(ACTIVITY_TYPE_LOCALIZED);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, trackBuilder.getTrack(), pair.second);
scenario = ActivityScenario.launch(AggregatedStatisticsActivity.class);
}
@After
public void tearDown() {
stopTrackRecordingService();
if (scenario != null) {
scenario.close();
}
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
}
@Test
public void espressoAggregatedFilterTest() {
waitUntilDisplayed(withId(R.id.bottom_app_bar), UI_TIMEOUT);
waitUntilDisplayed(withId(R.id.aggregated_statistics_filter), UI_TIMEOUT);
waitUntilRecyclerViewItemCount(withId(R.id.aggregated_stats_list), 1, UI_TIMEOUT);
waitUntilDisplayed(allOf(withId(R.id.aggregated_stats_type_label), withText(ACTIVITY_TYPE_LOCALIZED)), UI_TIMEOUT);
// open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button))
.perform(click());
// open FilterDialogFragment through the current bottom app bar action item
// open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.bottom_app_bar))
.check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter))
.perform(performClick());
.perform(click());
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
Matcher<View> activityTypeFilter = allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
onView(allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
withParent(allOf(withId(R.id.filter_items),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
isDisplayed());
onView(activityTypeFilter)
.inRoot(isDialog())
.check(matches(isDisplayed()));
isDisplayed()))
.perform(waitFor(2000));
// check there's an edit text for "from date"
onView(withId(R.id.filter_date_edit_text_from))
.inRoot(isDialog())
.check(matches(isDisplayed()));
// check there's an edit text for "to date"
onView(withId(R.id.filter_date_edit_text_to))
.inRoot(isDialog())
.check(matches(isDisplayed()));
}
}
@@ -63,8 +63,8 @@ public class RecordingLayoutTest extends TestCase {
RecordingLayout recordingLayout = RecordingLayoutIO.fromCsvLine(csv1, resources);
// then layout and data fields are built correctly
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -78,8 +78,8 @@ public class RecordingLayoutTest extends TestCase {
List<DataField> dataFieldList = recordingLayout.getFields();
// then layout and data fields are built correctly
assertEquals("Default Layout", recordingLayout.getName());
assertEquals(2, recordingLayout.getColumnsPerRow());
assertEquals(recordingLayout.getName(), "Default Layout");
assertEquals(recordingLayout.getColumnsPerRow(), 2);
assertEquals(PreferencesUtils.getDefaultLayout().getFields(), recordingLayout.getFields());
}
@@ -144,6 +144,6 @@ public class RecordingLayoutTest extends TestCase {
String csv = recordingLayout.toCsv();
// then csv is well built
assertEquals("Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;", csv);
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
}
}
@@ -35,10 +35,7 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(50, distanceInterval);
assertIntervalStatisticsComputation(computation, 50, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(50, distanceInterval);
}
/**
@@ -52,10 +49,7 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(200, distanceInterval);
assertIntervalStatisticsComputation(computation, 200, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(200, distanceInterval);
}
/**
@@ -69,10 +63,7 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 3000f;
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(3000, distanceInterval);
assertIntervalStatisticsComputation(computation, 3000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(3000, distanceInterval);
}
/**
@@ -86,10 +77,7 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 3000f;
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(1000, distanceInterval);
assertIntervalStatisticsComputation(computation, 1000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(1000, distanceInterval);
}
/**
@@ -103,10 +91,7 @@ public class IntervalStatisticsUpdaterTest {
float distanceInterval = 1000f;
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(10000, distanceInterval);
assertIntervalStatisticsComputation(computation, 10000, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(10000, distanceInterval);
}
@Test
@@ -137,18 +122,15 @@ public class IntervalStatisticsUpdaterTest {
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
// when and then
IntervalStatisticsComputation computation = computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
assertIntervalStatisticsComputation(computation, numberOfPoints, distanceInterval);
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
}
private IntervalStatisticsComputation computeIntervalStatistics(int numberOfPoints, float distanceInterval) {
private void whenAndThen(int numberOfPoints, float distanceInterval) {
Pair<Track, List<TrackPoint>> trackWithStats = buildTrackWithTrackPoints(numberOfPoints);
return computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
}
private IntervalStatisticsComputation computeIntervalStatistics(Track track, List<TrackPoint> trackPoints, float distanceInterval) {
private void whenAndThen(Track track, List<TrackPoint> trackPoints, int numberOfPoints, float distanceInterval) {
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(distanceInterval));
intervalStatistics.addTrackPoints(trackPoints.iterator());
@@ -175,27 +157,23 @@ public class IntervalStatisticsUpdaterTest {
}
}
return new IntervalStatisticsComputation(track, intervalList, totalDistance, totalTime, totalGain, totalLoss);
}
private void assertIntervalStatisticsComputation(IntervalStatisticsComputation computation, int numberOfPoints, float distanceInterval) {
assertEquals(computation.track.statistics().totalDuration().toSeconds(), computation.totalTime, 0.01);
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
assertEquals(computation.intervalList.size(), (int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval));
if (computation.totalGain != null && computation.totalLoss != null) {
assertEquals(computation.totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
assertEquals(computation.totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
// then
assertEquals(track.statistics().totalDuration().toSeconds(), totalTime, 0.01);
assertEquals(track.statistics().totalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(intervalList.size(), (int) Math.ceil(track.statistics().totalDistance().toM() / distanceInterval));
if (totalGain != null && totalLoss != null) {
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
} else {
assertTrue(computation.intervalList.stream().noneMatch(i -> i.altitudeGainLoss() != null));
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(i -> i.altitudeGainLoss() != null));
}
Distance remainingDistance = computation.totalDistance;
for (int i = 0; i < computation.intervalList.size() - 1; i++) {
assertEquals(computation.intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
remainingDistance = remainingDistance.minus(computation.intervalList.get(i).totalDistance());
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
totalDistance = totalDistance.minus(intervalList.get(i).totalDistance());
}
assertEquals(computation.intervalList.get(computation.intervalList.size() - 1).totalDistance().toM(), remainingDistance.toM(), 0.01);
assertEquals(intervalList.get(intervalList.size() - 1).totalDistance().toM(), totalDistance.toM(), 0.01);
}
private static Pair<Track, List<TrackPoint>> buildTrackWithTrackPoints(int numberOfPoints) {
@@ -217,27 +195,4 @@ public class IntervalStatisticsUpdaterTest {
return new Pair<>(track, trackPoints);
}
private static final class IntervalStatisticsComputation {
private final Track track;
private final List<Statistics> intervalList;
private final Distance totalDistance;
private final float totalTime;
private final Float totalGain;
private final Float totalLoss;
private IntervalStatisticsComputation(Track track,
List<Statistics> intervalList,
Distance totalDistance,
float totalTime,
Float totalGain,
Float totalLoss) {
this.track = track;
this.intervalList = intervalList;
this.totalDistance = totalDistance;
this.totalTime = totalTime;
this.totalGain = totalGain;
this.totalLoss = totalLoss;
}
}
}
}
@@ -1,107 +0,0 @@
package de.dennisguse.opentracks.util;
import static org.junit.Assert.fail;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public final class ActivityTestUtils {
private ActivityTestUtils() {
}
public static <T extends Activity> T waitForResumedActivity(Class<T> activityClass, Duration timeout) {
T resumedActivity = getResumedActivity(activityClass);
if (resumedActivity != null) {
return resumedActivity;
}
Application application = ApplicationProvider.getApplicationContext();
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<T> resumedActivityRef = new AtomicReference<>();
Application.ActivityLifecycleCallbacks callbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activityClass.isInstance(activity)) {
resumedActivityRef.set(activityClass.cast(activity));
latch.countDown();
}
}
@Override
public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
application.registerActivityLifecycleCallbacks(callbacks);
T currentResumedActivity = findResumedActivity(activityClass);
if (currentResumedActivity != null) {
resumedActivityRef.set(currentResumedActivity);
latch.countDown();
}
});
try {
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
fail("Timed out waiting for resumed activity " + activityClass.getSimpleName());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
fail("Interrupted while waiting for resumed activity " + activityClass.getSimpleName());
} finally {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> application.unregisterActivityLifecycleCallbacks(callbacks));
}
return resumedActivityRef.get();
}
private static <T extends Activity> T getResumedActivity(Class<T> activityClass) {
AtomicReference<T> resumedActivity = new AtomicReference<>();
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> resumedActivity.set(findResumedActivity(activityClass)));
return resumedActivity.get();
}
private static <T extends Activity> T findResumedActivity(Class<T> activityClass) {
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
if (activityClass.isInstance(activity)) {
return activityClass.cast(activity);
}
}
return null;
}
}
@@ -1,25 +1,14 @@
package de.dennisguse.opentracks.util;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static org.hamcrest.Matchers.allOf;
import android.app.Activity;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
@@ -27,112 +16,61 @@ import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
public class EspressoUtils {
public static Matcher<View> withRecyclerViewItemCount(final int count) {
public static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<>() {
@Override
protected boolean matchesSafely(View view) {
if (!(view instanceof RecyclerView recyclerView)) {
return false;
}
RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
return adapter != null && adapter.getItemCount() == count;
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public void describeTo(Description description) {
description.appendText("RecyclerView should have " + count + " items");
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
public static Matcher<View> withSelectedTab(final int index) {
public static Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<>() {
@Override
protected boolean matchesSafely(View view) {
return view instanceof TabLayout tabLayout
&& tabLayout.getSelectedTabPosition() == index;
public boolean matchesSafely(final View view) {
return ((ListView) view).getCount() == size;
}
@Override
public void describeTo(Description description) {
description.appendText("TabLayout should have selected tab " + index);
public void describeTo(final Description description) {
description.appendText("ListView should have " + size + " items");
}
};
}
public static Matcher<View> withViewPagerCurrentItem(final int index) {
return new TypeSafeMatcher<>() {
public static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
protected boolean matchesSafely(View view) {
return view instanceof ViewPager2 viewPager2
&& viewPager2.getCurrentItem() == index
&& viewPager2.getScrollState() == ViewPager2.SCROLL_STATE_IDLE;
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public void describeTo(Description description) {
description.appendText("ViewPager2 should be idle on item " + index);
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
public static void waitForResumedView(final Matcher<View> viewMatcher, final Duration timeout) {
waitForResumedViewCondition(viewMatcher, ignored -> true, "view to match " + viewMatcher, timeout);
}
public static void waitUntilDisplayed(final Matcher<View> viewMatcher, final Duration timeout) {
waitForResumedView(allOf(viewMatcher, isDisplayed()), timeout);
}
public static void waitUntilRecyclerViewItemCount(final Matcher<View> viewMatcher, final int expectedCount, final Duration timeout) {
waitForResumedViewCondition(
allOf(viewMatcher, isDisplayed()),
view -> withRecyclerViewItemCount(expectedCount).matches(view),
"RecyclerView item count to be " + expectedCount,
timeout);
}
public static void waitForResumedViewCondition(final Matcher<View> viewMatcher,
final Predicate<View> condition,
final String conditionDescription,
final Duration timeout) {
long deadline = SystemClock.uptimeMillis() + timeout.toMillis();
do {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
AtomicBoolean matched = new AtomicBoolean(false);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
View decorView = activity.getWindow() != null ? activity.getWindow().getDecorView() : null;
if (decorView == null) {
continue;
}
for (View child : TreeIterables.breadthFirstViewTraversal(decorView)) {
if (viewMatcher.matches(child) && condition.test(child)) {
matched.set(true);
return;
}
}
}
});
if (matched.get()) {
return;
}
} while (SystemClock.uptimeMillis() < deadline);
throw new AssertionError("Timed out waiting for " + conditionDescription);
}
public static ViewAction selectTabAtIndex(final int index) {
return new ViewAction() {
@Override
@@ -153,135 +91,4 @@ public class EspressoUtils {
};
}
public static ViewAction performClick() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke View.performClick()";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
if (!view.performClick()) {
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
uiController.loopMainThreadUntilIdle();
}
};
}
public static ViewAction performClickOnAncestor() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke performClick() on the matched view or its ancestor";
}
@Override
public Matcher<View> getConstraints() {
return isAssignableFrom(View.class);
}
@Override
public void perform(UiController uiController, View view) {
View current = view;
while (current != null) {
if (current.performClick()) {
uiController.loopMainThreadUntilIdle();
return;
}
ViewParent parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
current = view;
while (current != null) {
ViewParent parent = current.getParent();
if (parent instanceof AdapterView<?> adapterView) {
int position = adapterView.getPositionForView(current);
if (position != AdapterView.INVALID_POSITION
&& adapterView.performItemClick(current, position, adapterView.getAdapter().getItemId(position))) {
uiController.loopMainThreadUntilIdle();
return;
}
}
current = parent instanceof View ? (View) parent : null;
}
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
};
}
public static ViewAction performLongClick() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke View.performLongClick()";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
if (!view.performLongClick()) {
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
uiController.loopMainThreadUntilIdle();
}
};
}
public static ViewAction performLongClickOnAncestor() {
return new ViewAction() {
@Override
public String getDescription() {
return "invoke performLongClick() on the matched view or its ancestor";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
View current = view;
while (current != null) {
if (current.performLongClick()) {
uiController.loopMainThreadUntilIdle();
return;
}
ViewParent parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
throw new PerformException.Builder()
.withActionDescription(getDescription())
.withViewDescription(HumanReadables.describe(view))
.build();
}
};
}
}

Some files were not shown because too many files have changed in this diff Show More