forked from upstream-mirrors/OpenTracks
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edba2294f3 | |||
| 5743f29aed | |||
| 7b2ad1590c | |||
| 251a26d2f8 | |||
| fd362e17e1 | |||
| bf0dd9cda4 | |||
| 0fe176fa24 | |||
| 9b45a70e67 | |||
| bd1b68dffd | |||
| 3c5c69fdb0 | |||
| 87d7ebcb64 | |||
| a01edcc9e8 | |||
| 8b7063d18b | |||
| 25d4daa295 | |||
| 5dbd17f3bb | |||
| fd914630d0 | |||
| caf8b5526d | |||
| 8d5a7e7e8b | |||
| 9fb84cc47c | |||
| 5c7731bb1f | |||
| ce21fe3569 | |||
| d47fa27e02 | |||
| 58d79380cf | |||
| a0ab2e2896 | |||
| 0bc526df06 | |||
| f51edbf712 | |||
| 2384b947b3 | |||
| aca7f9063e | |||
| 78282b97ed | |||
| c32ab408e6 | |||
| 15f3ccbcbd | |||
| f2594371d3 | |||
| 4d54472c93 | |||
| 3a2a62a6d7 | |||
| 17487961ad | |||
| af268c608a | |||
| 605d4c02f5 | |||
| cae2de0982 | |||
| 257b051f13 | |||
| 0cf65e0b89 | |||
| 062d48e69f | |||
| a270b1f64f | |||
| f7bc25c17d | |||
| 90f82f7c5c | |||
| 062457f1aa | |||
| cd4d297608 | |||
| 06c87bc7d4 | |||
| df98fdd4de |
@@ -10,7 +10,7 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
|
||||
+6
-6
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.0-rc01'
|
||||
classpath 'com.android.tools.build:gradle:8.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,12 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 5506
|
||||
versionName "v4.9.4"
|
||||
versionCode 5554
|
||||
versionName "v4.9.6"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
minSdk 24
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
|
||||
@@ -125,7 +125,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
@@ -141,7 +141,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.6.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.8.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.4: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Pád na systému Android 14 při neudělení oprávnění
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.5: OpenTracks
|
||||
|
||||
Oprava chyby:
|
||||
- nastavení oznámení o nečinnosti opět funguje
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.5: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- idle announcement setting is working again
|
||||
@@ -0,0 +1,7 @@
|
||||
v4.9.6: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Export: existing files is now completely overwriting
|
||||
|
||||
Developer:
|
||||
- Refactored barometric data processing
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.5: OpenTracks
|
||||
|
||||
Corrección de errores:
|
||||
- La configuración del anuncio inactivo vuelve a funcionar
|
||||
@@ -0,0 +1,10 @@
|
||||
(versionName): OpenTracks
|
||||
|
||||
Cambios:
|
||||
-
|
||||
|
||||
Corrección de errores:
|
||||
-
|
||||
|
||||
Desarrollador:
|
||||
-
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.4: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash op Android 14 als er geen toestemming is verleend
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.5: OpenTracks
|
||||
|
||||
Bugfix :
|
||||
- inactieve aankondiging werkt weer
|
||||
@@ -0,0 +1,15 @@
|
||||
v3.0.0: OpenTracks
|
||||
|
||||
Первая версия OpenTracks (т.е. полное отделение от MyTracks)
|
||||
|
||||
Изменения:
|
||||
- ребрендинг MyTracks в OpenTracks
|
||||
- добавлена поддержка пульсометров Bluetooth LE
|
||||
- удалены все возможности, требующие доступа в Интернет
|
||||
- удалена интеграция с Google Maps
|
||||
- удалено использование сервисов Google Play
|
||||
- удалена поддержка датчиков ANT+
|
||||
- удалена поддержка датчиков Bluetooth classic
|
||||
- удален Protobuf
|
||||
- удалены расчет калорий и определение типа активности
|
||||
- удален интерфейс MyTracks Service API (т.е. больше нет интеграции с другими приложениями)
|
||||
@@ -0,0 +1,17 @@
|
||||
v3.0.1: OpenTracks
|
||||
|
||||
Изменения:
|
||||
- можно делиться маркерами
|
||||
- изменена структура настроек
|
||||
- удалена кнопка "Назад" типа actionbar
|
||||
- экспорт: убрана настройка размера фото
|
||||
- экспорт: теперь можно выбрать формат KML
|
||||
|
||||
Исправление ошибок:
|
||||
- папка экспорта теперь - /OpenTracks/
|
||||
- ландшафтный режим больше не приводит к сбою
|
||||
- исправлен экспорт KML/KMZ
|
||||
|
||||
Для разработчиков:
|
||||
- тесты можно выполнить (но многие с ошибками)
|
||||
- включена постоянная интеграция с TravisCI (без тестов)
|
||||
@@ -0,0 +1,7 @@
|
||||
v4.8.4: OpenTracks
|
||||
|
||||
Изменения:
|
||||
- показывается макс. мощность для записанных треков
|
||||
|
||||
Для разработчиков:
|
||||
- TrackList теперь использует RecyclerView
|
||||
@@ -0,0 +1,23 @@
|
||||
v4.9.0: OpenTracks
|
||||
|
||||
ВНИМАНИЕ: СУЩЕСТВЕННОЕ ИЗМЕНЕНИЕ
|
||||
|
||||
В этой версии меняется порядок определения OpenTracks состояния движения.
|
||||
До настоящего времени скорость (от GPS или датчика) использовалась с применением порогового значения (например, менее 0,3 м/с).
|
||||
Однако, это может приводить к получению очень "шумных" ("грязных") данных.
|
||||
Теперь же используется длительность таймаута (т.е. период времени без существенных изменений местоположения длительностью X секунд).
|
||||
Эта длительность задается пользователем.
|
||||
События перехода в режим без движения сохраняются в базе данных, включая метки времени, поэтому статистика может быть пересчитана.
|
||||
|
||||
Возврат к предыдущей версии от этой не предусмотрен.
|
||||
Пожалуйста, выполните резервную копию данных перед обновлением.
|
||||
|
||||
Изменения:
|
||||
- определение того, что пользователь не двигается, на основе периода "без движения" вместо порога минимальной скорости
|
||||
- добавлен вид активности "самокат"
|
||||
|
||||
Исправление ошибок:
|
||||
- во время записи в списке треков индикация записи отображалась на всех треках
|
||||
|
||||
Для разработчиков:
|
||||
- снова включены тесты выполнения действий Github
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.1: OpenTracks
|
||||
|
||||
Исправление ошибок:
|
||||
- сбой при выборе нескольких элементов в списке треков
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.9.2: OpenTracks
|
||||
|
||||
Изменения:
|
||||
- возможность использовать динамические цвета Android 12 (Material Design)
|
||||
- поддержка предикативного действия "назад" Android 14 для некоторых ситуаций
|
||||
|
||||
Исправление ошибок:
|
||||
- настройки для голосового уведомления об общем расстоянии ошибочно удалялись
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.3: OpenTracks
|
||||
|
||||
Исправление ошибок:
|
||||
- предотвращение сбоя из-за того, что toList() не всегда доступен
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.9.4: OpenTracks
|
||||
|
||||
Исправление ошибок:
|
||||
- сбой на Android 14, если не предоставлены разрешения
|
||||
@@ -0,0 +1,10 @@
|
||||
(версия): OpenTracks
|
||||
|
||||
Изменения:
|
||||
-
|
||||
|
||||
Исправление ошибок:
|
||||
-
|
||||
|
||||
Для разработчиков:
|
||||
-
|
||||
@@ -1,22 +1,22 @@
|
||||
Спортивный трекер уважающий вашу приватность.
|
||||
Спортивный трекер, уважающий вашу приватность.
|
||||
|
||||
Спорт и иные активности дарят радость, мастерство и уверенность в себе.
|
||||
|
||||
Цените свое здоровье, следя за тренировками.
|
||||
Записывает, как вы бегаете или идете, и предоставляет вам функционал велокомпьютера с большим экраном для езды на велосипеде.
|
||||
Цените свое здоровье, отслеживая тренировки.
|
||||
Записывайте, как вы бегаете или прогуливаетесь, и получите функционал велокомпьютера с большим экраном.
|
||||
Отмечайте интересные места на своем пути с помощью фотографий.
|
||||
Храните записанные данные в мельчайших подробностях для анализа.
|
||||
Делитесь только теми данными, которые хотите, чтобы другие имели доступ к ним.
|
||||
Делитесь с другими только теми данными, которые выберете сами.
|
||||
|
||||
* Голосовые уведомления.
|
||||
* Поддержка датчиков Bluetooth LE: пульс, скорость и дистанция (для велосипеда), частота вращения педалей (для велосипеда) и измеритель мощности (для велосипеда).
|
||||
* Поддержка датчиков Bluetooth LE: пульс, скорость и расстояние (для велосипеда), частота вращения педалей (для велосипеда) и мощность (для велосипеда).
|
||||
* Набор и потеря высоты: с помощью барометрического датчика.
|
||||
* Высота указана в EGM2008 (над средним уровнем моря); экспортируется как WGS84.
|
||||
* Экспорт данных в виде треков формата KMZ (включая фотографии), KML или GPX.
|
||||
* Не требует соединения с Интернетом и лишних разрешений.
|
||||
* Экспорт данных в форматах KMZ (включая фотографии), KML или GPX.
|
||||
* Не требуются соединение с Интернетом и лишние разрешения.
|
||||
* Темная и светлая темы с учетом системных настроек.
|
||||
* Без рекламы.
|
||||
|
||||
Libre software / Свободное программное обеспечение / Открытый исходный код
|
||||
означает, что вы можете использовать, изучать, изменять и делиться исходным кодом.
|
||||
означает, что вы можете использовать, изучать, изменять исходный код и делиться им.
|
||||
Лицензия Apache 2.0
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -202,11 +202,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
+52
-40
@@ -48,6 +48,7 @@ import de.dennisguse.opentracks.TimezoneRule;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
@@ -58,11 +59,12 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
@@ -71,14 +73,12 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
/**
|
||||
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
|
||||
* <p>
|
||||
* Note: those tests are affected by {@link de.dennisguse.opentracks.sensors.sensorData.SensorData}.isRecent().
|
||||
* Note: those tests are affected by {@link Aggregator}.isRecent().
|
||||
* If the test device is too slow (like in a CI) these are likely to fail as the sensor data will be omitted from actual.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExportImportTest {
|
||||
|
||||
private static final String TAG = ExportImportTest.class.getSimpleName();
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@@ -139,23 +139,22 @@ public class ExportImportTest {
|
||||
|
||||
Distance sensorDistance = Distance.of(10); // recording distance interval
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1f);
|
||||
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
|
||||
|
||||
// A sensor-only TrackPoint
|
||||
trackPointCreator.setClock("2020-02-02T02:02:04Z");
|
||||
mockAltitudeChange(trackPointCreator, 1);
|
||||
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
|
||||
mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:14Z");
|
||||
mockBLESensorData(trackPointCreator, 15f, null, 67f, 3f, 50f);
|
||||
mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null);
|
||||
trackPointCreator.setClock("2020-02-02T02:02:15Z");
|
||||
mockBLESensorData(trackPointCreator, null, null, 68f, 3f, 50f);
|
||||
mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:16Z");
|
||||
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f); // Distance will be added to next TrackPoint
|
||||
mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0f);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||
@@ -165,15 +164,16 @@ public class ExportImportTest {
|
||||
trackPointCreator.setClock("2020-02-02T02:03:20Z");
|
||||
service.resumeTrack(trackId);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0f);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0f);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:03:30Z");
|
||||
service.getTrackRecordingManager().onIdle();
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0f);
|
||||
|
||||
trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
|
||||
trackPointCreator.setClock("2020-02-02T02:04:00Z");
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -271,7 +271,9 @@ public class ExportImportTest {
|
||||
.setAltitudeLoss(0f)
|
||||
.setAltitudeGain(0f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z")),
|
||||
new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z"))
|
||||
.setAltitudeLoss(0f)
|
||||
.setAltitudeGain(0f),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:50Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(16.001)
|
||||
@@ -281,7 +283,6 @@ public class ExportImportTest {
|
||||
.setAltitudeGain(0f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
|
||||
|
||||
), actual);
|
||||
}
|
||||
|
||||
@@ -545,40 +546,51 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
|
||||
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) {
|
||||
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
|
||||
|
||||
SensorDataSet sensorDataSet = new SensorDataSet();
|
||||
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
|
||||
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
|
||||
AggregatorCyclingPower cyclingPower = Mockito.mock(AggregatorCyclingPower.class);
|
||||
Mockito.when(cyclingPower.hasValue()).thenReturn(true);
|
||||
Mockito.when(cyclingPower.getValue()).thenReturn(Power.of(power));
|
||||
sensorDataSet.add(cyclingPower);
|
||||
|
||||
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
|
||||
AggregatorHeartRate agheartRate = Mockito.mock(AggregatorHeartRate.class);
|
||||
Mockito.when(agheartRate.getValue()).thenReturn(HeartRate.of(heartRate));
|
||||
sensorDataSet.add(agheartRate);
|
||||
|
||||
AggregatorCyclingCadence cyclingCadence = Mockito.mock(AggregatorCyclingCadence.class);
|
||||
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
|
||||
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
|
||||
sensorDataSet.set(cyclingCadence);
|
||||
sensorDataSet.add(cyclingCadence);
|
||||
|
||||
if (distance != null && speed != null) {
|
||||
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
|
||||
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
|
||||
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
|
||||
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
|
||||
AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class);
|
||||
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
|
||||
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
|
||||
sensorDataSet.set(distanceSpeed);
|
||||
Mockito.when(distanceSpeed.getValue()).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed)));
|
||||
sensorDataSet.add(distanceSpeed);
|
||||
} else {
|
||||
sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", ""));
|
||||
}
|
||||
|
||||
trackPointCreator.getSensorManager().sensorDataSet = sensorDataSet;
|
||||
mockAltitudeChange(trackPointCreator, altitudeGain);
|
||||
|
||||
trackPointCreator.onChange(sensorDataSet);
|
||||
|
||||
//TODO Might require: getSensorManager().sensorDataSet = new SensorDataSet()
|
||||
}
|
||||
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
|
||||
AltitudeSumManager altitudeSumManager = trackPointCreator.getSensorManager().getAltitudeSumManager();
|
||||
altitudeSumManager.setAltitudeGain_m(altitudeGain);
|
||||
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
|
||||
SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet;
|
||||
|
||||
if (altitudeGain != null) {
|
||||
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
|
||||
Mockito.when(barometer.hasValue()).thenReturn(true);
|
||||
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
|
||||
sensorDataSet.add(barometer);
|
||||
} else {
|
||||
sensorDataSet.add(new AggregatorBarometer(""));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, float altitudeGain) {
|
||||
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, Float altitudeGain) {
|
||||
Location location = new Location("mock");
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
|
||||
|
||||
/**
|
||||
* NOTE: Test data is completely artificial.
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class AltitudeSumManagerTest {
|
||||
|
||||
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
|
||||
for (float f : values) {
|
||||
altitudeSumManager.onSensorValueChanged(AtmosphericPressure.ofHPA(f));
|
||||
}
|
||||
}
|
||||
|
||||
@Mock
|
||||
BarometerInternal driver;
|
||||
|
||||
@Test
|
||||
public void getAltitudeGainLoss_downhill() {
|
||||
// given
|
||||
AltitudeSumManager subject = new AltitudeSumManager(driver);
|
||||
|
||||
when(driver.isConnected()).thenReturn(true);
|
||||
|
||||
// then
|
||||
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
|
||||
Assert.assertEquals(15f, subject.getAltitudeLoss_m(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sensorUnavailable() {
|
||||
// given
|
||||
AltitudeSumManager subject = new AltitudeSumManager(driver);
|
||||
|
||||
when(driver.isConnected()).thenReturn(true);
|
||||
|
||||
// then
|
||||
subject.onSensorValueChanged(AtmosphericPressure.ofHPA(999f));
|
||||
|
||||
// then
|
||||
Assert.assertNull(subject.getAltitudeGain_m());
|
||||
Assert.assertNull(subject.getAltitudeLoss_m());
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
|
||||
public class BluetoothHandlerBarometricPressureTest {
|
||||
|
||||
@Test
|
||||
public void parseEnvironmentalSensing_Pa() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
|
||||
|
||||
// when
|
||||
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(AtmosphericPressure.ofPA(100165), pressure);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BluetoothHandlerCyclingDistanceSpeedTest {
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertNull(sensor.first);
|
||||
assertEquals(200, sensor.second.crankRevolutionsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
|
||||
assertNull(sensor.second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
|
||||
assertEquals(225, sensor.second.crankRevolutionsCount());
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BluetoothHandlerCyclingPowerTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, powerCadence.power().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
|
||||
|
||||
// when
|
||||
BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(0, powerCadence.power().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
|
||||
public class BluetoothHandlerManagerHeartRateTest {
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(60), heartRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(257), heartRate);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
|
||||
public class BluetoothHandlerRunningSpeedAndCadenceTest {
|
||||
|
||||
@Test
|
||||
public void parseRunningSpeedAndCadence_with_distance() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
|
||||
|
||||
// when
|
||||
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(Speed.of(5), sensor.speed());
|
||||
assertEquals(Cadence.of(80), sensor.cadence());
|
||||
assertEquals(Distance.of(6553.5 + 1677721.6), sensor.totalDistance());
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(60), heartRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(257), heartRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseEnvironmentalSensing_Pa() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
|
||||
|
||||
// when
|
||||
AtmosphericPressure pressure = BluetoothUtils.parseEnvironmentalSensing(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(AtmosphericPressure.ofPA(100165), pressure);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertNull(sensor.getDistanceSpeed());
|
||||
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
assertNull(sensor.getCadence());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
assertEquals(225, sensor.getCadence().getCrankRevolutionsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, powerCadence.power().getValue().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
|
||||
|
||||
// when
|
||||
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(0, powerCadence.power().getValue().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.cadence().getCrankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.cadence().getCrankRevolutionsTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseRunningSpeedAndCadence_with_distance() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
|
||||
|
||||
// when
|
||||
SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(Speed.of(5), sensor.getSpeed());
|
||||
assertEquals(Cadence.of(80), sensor.getCadence());
|
||||
assertEquals(Distance.of(6553.5 + 1677721.6), sensor.getTotalDistance());
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
|
||||
public class AggregatorBarometerTest {
|
||||
|
||||
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
|
||||
for (float f : values) {
|
||||
aggregatorBarometer.add(new Raw<>(AtmosphericPressure.ofHPA(f)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAltitudeGainLoss_downhill() {
|
||||
// given
|
||||
AggregatorBarometer subject = new AggregatorBarometer("");
|
||||
|
||||
// then
|
||||
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0f, subject.value.gain_m(), 0.01);
|
||||
Assert.assertEquals(15f, subject.value.loss_m(), 0.01);
|
||||
}
|
||||
}
|
||||
+37
-94
@@ -2,17 +2,17 @@ package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -20,12 +20,11 @@ public class SensorDataCyclingTest {
|
||||
|
||||
@Test
|
||||
public void compute_cadence_1() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getValue().getRPM(), 0.01);
|
||||
@@ -33,12 +32,11 @@ public class SensorDataCyclingTest {
|
||||
|
||||
@Test
|
||||
public void compute_cadence_2() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016);
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
|
||||
|
||||
// then
|
||||
assertEquals(33.53, current.getValue().getRPM(), 0.01);
|
||||
@@ -46,12 +44,11 @@ public class SensorDataCyclingTest {
|
||||
|
||||
@Test
|
||||
public void compute_cadence_sameCount() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048);
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
|
||||
|
||||
// then
|
||||
assertEquals(Cadence.of(0), current.getValue());
|
||||
@@ -60,126 +57,72 @@ public class SensorDataCyclingTest {
|
||||
|
||||
@Test
|
||||
public void compute_cadence_sameTime() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024);
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
|
||||
|
||||
// then
|
||||
assertFalse(current.hasValue());
|
||||
assertFalse(current.hasValue()); //TODO Cadence should be 0?
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_cadence_rollOverTime() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0);
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getValue().getRPM(), 0.01);
|
||||
}
|
||||
|
||||
@Ignore("Disabled from #953")
|
||||
@Test
|
||||
@Deprecated
|
||||
public void compute_cadence_rollOverCount() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getValue().getRPM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_cadence_overflow() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
|
||||
|
||||
// then
|
||||
// TODO See #953
|
||||
// assertEquals(60, current.getValue().getRPM(), 0.01);
|
||||
assertNull(current.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_speed() {
|
||||
// given
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
|
||||
current.setWheelCircumference(Distance.ofMM(2150));
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2150));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
|
||||
|
||||
// then
|
||||
assertEquals(2.15, current.getValue().getDistance().toM(), 0.01);
|
||||
assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01);
|
||||
assertEquals(2.15, current.getValue().distance().toM(), 0.01);
|
||||
assertEquals(1.20, current.getValue().speed().toMPS(), 0.01);
|
||||
}
|
||||
|
||||
@Ignore("Disabled from #953")
|
||||
@Test
|
||||
@Deprecated
|
||||
public void compute_speed_rollOverCount() {
|
||||
// given
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
|
||||
current.setWheelCircumference(Distance.ofMM(2000));
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
|
||||
// then
|
||||
assertEquals(2, current.getValue().getDistance().toM(), 0.01);
|
||||
assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_speed_overflow() {
|
||||
// given
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2000));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
|
||||
current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
|
||||
|
||||
|
||||
// then
|
||||
// TODO See #953
|
||||
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
|
||||
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
|
||||
assertNull(current.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_speed_with_no_data() {
|
||||
// given
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress");
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
// then
|
||||
assertNotEquals(previous, current);
|
||||
assertNotEquals(previous, previous);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_cadence_with_no_data() {
|
||||
// given
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress");
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
// then
|
||||
assertNotEquals(previous, current);
|
||||
assertNotEquals(previous, previous);
|
||||
}
|
||||
}
|
||||
+48
-39
@@ -7,7 +7,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
@@ -19,6 +18,7 @@ import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@@ -28,24 +28,25 @@ import java.util.concurrent.TimeoutException;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
/**
|
||||
* Tests insert location.
|
||||
* Note: on API23, the TrackRecordingService may not be stopped properly before the next test.
|
||||
* So, if something fails, subsequent tests may be affected.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackRecordingServiceRecordingTest {
|
||||
@@ -58,14 +59,6 @@ public class TrackRecordingServiceRecordingTest {
|
||||
|
||||
private TrackRecordingService service;
|
||||
|
||||
private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() {
|
||||
@Override
|
||||
public void fill(@NonNull TrackPoint trackPoint) {
|
||||
trackPoint.setAltitudeGain(0f);
|
||||
trackPoint.setAltitudeLoss(0f);
|
||||
}
|
||||
};
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
@@ -105,7 +98,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
String startTime = "2020-02-02T02:02:02Z";
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// then
|
||||
assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null)
|
||||
@@ -142,7 +136,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// when
|
||||
String pauseTime = "2020-02-02T02:02:03Z";
|
||||
@@ -188,7 +183,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
String starTime = "2020-02-02T02:02:02Z";
|
||||
trackPointCreator.setClock(starTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
String pauseTime = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(pauseTime);
|
||||
@@ -220,7 +216,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
String stopTime = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(stopTime);
|
||||
@@ -230,7 +227,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
String resumeTime = "2020-02-02T02:02:04Z";
|
||||
trackPointCreator.setClock(resumeTime);
|
||||
service.resumeTrack(trackId);
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// then
|
||||
new TrackPointAssert().assertEquals(List.of(
|
||||
@@ -251,18 +249,19 @@ public class TrackRecordingServiceRecordingTest {
|
||||
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
sensorManager.sensorDataSet.add(new AggregatorHeartRate("", ""));
|
||||
// when
|
||||
String sensor1 = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(sensor1);
|
||||
|
||||
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
|
||||
sensorManager.onChanged(new Raw<>(HeartRate.of(5))); //Should be ignored
|
||||
|
||||
String sensor3 = "2020-02-02T02:02:13Z";
|
||||
trackPointCreator.setClock(sensor3);
|
||||
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
|
||||
sensorManager.onChanged(new Raw<>(HeartRate.of(7)));
|
||||
|
||||
String stopTime = "2020-02-02T02:02:15Z";
|
||||
trackPointCreator.setClock(stopTime);
|
||||
@@ -282,7 +281,6 @@ public class TrackRecordingServiceRecordingTest {
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_gpsOnly_recordingDistance_above() {
|
||||
@@ -291,7 +289,7 @@ public class TrackRecordingServiceRecordingTest {
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// when
|
||||
@@ -365,7 +363,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
@@ -429,7 +428,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
@@ -479,7 +479,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
@@ -525,7 +526,7 @@ public class TrackRecordingServiceRecordingTest {
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
@@ -581,25 +582,25 @@ public class TrackRecordingServiceRecordingTest {
|
||||
@Test
|
||||
public void testRecording_gpsAndSensor_gpsIdleMoving_sensorMoving() {
|
||||
// TODO Check TrackStatistics
|
||||
AltitudeSumManager altitudeSumManager = new AltitudeSumManager();
|
||||
|
||||
// given
|
||||
String startTime = "2020-02-02T02:02:02Z";
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager);
|
||||
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager();
|
||||
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
sensorManager.sensorDataSet.add(new AggregatorRunning("", ""));
|
||||
sensorManager.sensorDataSet.barometer = null;
|
||||
|
||||
// when
|
||||
String sensor1 = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(sensor1);
|
||||
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0))); //Should be ignored
|
||||
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(0)))); //Should be ignored
|
||||
|
||||
// when
|
||||
String sensor2 = "2020-02-02T02:02:04Z";
|
||||
trackPointCreator.setClock(sensor2);
|
||||
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2)));
|
||||
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2))));
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:05Z";
|
||||
@@ -608,12 +609,12 @@ public class TrackRecordingServiceRecordingTest {
|
||||
// when
|
||||
String sensor3 = "2020-02-02T02:02:06Z";
|
||||
trackPointCreator.setClock(sensor3);
|
||||
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12)));
|
||||
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12))));
|
||||
|
||||
// when
|
||||
String sensor4 = "2020-02-02T02:02:07Z";
|
||||
trackPointCreator.setClock(sensor4);
|
||||
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14))); //Should be ignored
|
||||
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored
|
||||
|
||||
// when
|
||||
String gps2 = "2020-02-02T02:02:08Z";
|
||||
@@ -622,7 +623,7 @@ public class TrackRecordingServiceRecordingTest {
|
||||
// when
|
||||
String sensor5 = "2020-02-02T02:02:10Z";
|
||||
trackPointCreator.setClock(sensor5);
|
||||
remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16))); //Should be ignored
|
||||
sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored
|
||||
|
||||
// when
|
||||
String gps3 = "2020-02-02T02:02:12Z";
|
||||
@@ -671,4 +672,12 @@ public class TrackRecordingServiceRecordingTest {
|
||||
.setSensorDistance(Distance.of(0))
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
|
||||
AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class);
|
||||
Mockito.when(barometer.hasValue()).thenReturn(true);
|
||||
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
|
||||
|
||||
trackPointCreator.getSensorManager().sensorDataSet.barometer = barometer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:03:21+01:00","TRACKPOINT",3,14.002,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3,16,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,,,,,,
|
||||
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,0,0,,,,
|
||||
"2020-02-02T03:03:50+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
|
||||
|
@@ -379,8 +379,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
startGpsMenuItem.setVisibility(!isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (!isRecording) {
|
||||
startGpsMenuItem.setIcon(AppCompatResources.getDrawable(this, isGpsStarted ? gpsStatusValue.icon : R.drawable.ic_gps_off_24dp));
|
||||
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable) {
|
||||
((AnimatedVectorDrawable) startGpsMenuItem.getIcon()).start();
|
||||
if (startGpsMenuItem.getIcon() instanceof AnimatedVectorDrawable animatedVectorDrawable) {
|
||||
animatedVectorDrawable.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +416,7 @@ public class TrackDataHub {
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
//TODO Could be @NonNull
|
||||
void onTrackUpdated(Track track);
|
||||
void onTrackUpdated(@NonNull Track track);
|
||||
|
||||
/**
|
||||
* Called to clear previously-sent track points.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
public record AltitudeGainLoss(float gain_m, float loss_m) {}
|
||||
@@ -38,15 +38,6 @@ public record Speed(double speed_mps) {
|
||||
return speed2;
|
||||
}
|
||||
|
||||
public static Speed absDiff(Speed speed1, Speed speed2) {
|
||||
//TODO Why Math.abs? Seems to be a leftover.
|
||||
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
|
||||
}
|
||||
|
||||
public Speed mul(double factor) {
|
||||
return new Speed(factor * speed_mps);
|
||||
}
|
||||
|
||||
public boolean isZero() {
|
||||
return speed_mps == 0;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
|
||||
private void loadData(List<DocumentFile> documentFiles) {
|
||||
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
|
||||
.map(FileUtils::getFiles)
|
||||
// TODO flatMap(Collection::stream) fails with ClassCastException; try in the future again
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<DocumentFile> fileList = new ArrayList<>();
|
||||
|
||||
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -228,7 +229,6 @@ public class KmzTrackImporter {
|
||||
* @param trackId the track's id which image belongs to.
|
||||
* @param fileName the file name
|
||||
*/
|
||||
@Deprecated //TODO Use JDK9's inputStream.transferTo() instead of manual buffer
|
||||
private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException {
|
||||
if (trackId == null || "".equals(fileName)) {
|
||||
return;
|
||||
@@ -238,10 +238,14 @@ public class KmzTrackImporter {
|
||||
File file = new File(dir, fileName);
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int count;
|
||||
while ((count = zipInputStream.read(buffer)) != -1) {
|
||||
fileOutputStream.write(buffer, 0, count);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
zipInputStream.transferTo(fileOutputStream);
|
||||
} else {
|
||||
byte[] buffer = new byte[4096];
|
||||
int count;
|
||||
while ((count = zipInputStream.read(buffer)) != -1) {
|
||||
fileOutputStream.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
*/
|
||||
public class AltitudeSumManager implements SensorConnector {
|
||||
|
||||
private static final String TAG = AltitudeSumManager.class.getSimpleName();
|
||||
|
||||
private final BarometerInternal driver;
|
||||
|
||||
private AtmosphericPressure lastAcceptedSensorValue;
|
||||
|
||||
private AtmosphericPressure lastSeenSensorValue;
|
||||
|
||||
private Float altitudeGain_m;
|
||||
private Float altitudeLoss_m;
|
||||
|
||||
public AltitudeSumManager() {
|
||||
driver = new BarometerInternal();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AltitudeSumManager(BarometerInternal mock) {
|
||||
this.driver = mock;
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
driver.connect(context, handler, this);
|
||||
|
||||
lastAcceptedSensorValue = null;
|
||||
reset();
|
||||
}
|
||||
|
||||
public void stop(Context context) {
|
||||
Log.d(TAG, "Stop");
|
||||
|
||||
driver.disconnect(context);
|
||||
reset();
|
||||
}
|
||||
|
||||
public void fill(@NonNull TrackPoint trackPoint) {
|
||||
trackPoint.setAltitudeGain(altitudeGain_m);
|
||||
trackPoint.setAltitudeLoss(altitudeLoss_m);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Float getAltitudeGain_m() {
|
||||
return driver.isConnected() ? altitudeGain_m : null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAltitudeGain_m(float altitudeGain_m) {
|
||||
this.altitudeGain_m = altitudeGain_m;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Float getAltitudeLoss_m() {
|
||||
return driver.isConnected() ? altitudeLoss_m : null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAltitudeLoss_m(float altitudeLoss_m) {
|
||||
this.altitudeLoss_m = altitudeLoss_m;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Log.d(TAG, "Reset");
|
||||
altitudeGain_m = null;
|
||||
altitudeLoss_m = null;
|
||||
}
|
||||
|
||||
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
|
||||
if (lastAcceptedSensorValue == null) {
|
||||
lastAcceptedSensorValue = currentSensorValue;
|
||||
lastSeenSensorValue = currentSensorValue;
|
||||
return;
|
||||
}
|
||||
|
||||
altitudeGain_m = altitudeGain_m != null ? altitudeGain_m : 0;
|
||||
altitudeLoss_m = altitudeLoss_m != null ? altitudeLoss_m : 0;
|
||||
|
||||
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, lastSeenSensorValue, currentSensorValue);
|
||||
if (altitudeChange != null) {
|
||||
altitudeGain_m += altitudeChange.getAltitudeGain_m();
|
||||
|
||||
altitudeLoss_m += altitudeChange.getAltitudeLoss_m();
|
||||
|
||||
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
|
||||
}
|
||||
|
||||
lastSeenSensorValue = currentSensorValue;
|
||||
|
||||
Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m);
|
||||
}
|
||||
}
|
||||
+20
-56
@@ -16,6 +16,7 @@
|
||||
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
@@ -24,29 +25,27 @@ import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
/**
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
* Also parses the transferred data into {@link SensorDataObserver}.
|
||||
*/
|
||||
public abstract class AbstractBluetoothConnectionManager<DataType> {
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BluetoothConnectionManager {
|
||||
|
||||
private static final String TAG = AbstractBluetoothConnectionManager.class.getSimpleName();
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
private final SensorDataObserver observer;
|
||||
private final SensorManager.SensorDataChangedObserver observer;
|
||||
|
||||
private final List<ServiceMeasurementUUID> serviceMeasurementUUIDs;
|
||||
private final SensorHandlerInterface sensorHandler;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@@ -76,7 +75,7 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService gattService = null;
|
||||
ServiceMeasurementUUID serviceMeasurement = null;
|
||||
for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) {
|
||||
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
|
||||
gattService = gatt.getService(s.serviceUUID());
|
||||
if (gattService != null) {
|
||||
serviceMeasurement = s;
|
||||
@@ -112,37 +111,25 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
UUID serviceUUID = characteristic.getService().getUuid();
|
||||
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress() + " with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
|
||||
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = serviceMeasurementUUIDs.stream()
|
||||
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid())).findFirst();
|
||||
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = sensorHandler.getServices()
|
||||
.stream()
|
||||
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid()))
|
||||
.findFirst();
|
||||
if (serviceMeasurementUUID.isEmpty()) {
|
||||
Log.e(TAG, "Unknown service UUID; not supported?");
|
||||
return;
|
||||
}
|
||||
|
||||
SensorData<DataType> sensorData = parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
|
||||
if (sensorData != null) {
|
||||
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
observer.onChanged(sensorData);
|
||||
} else {
|
||||
//TODO This might lead to NPEs in case of race conditions due to shutdown.
|
||||
observer.getHandler().post(() -> observer.onChanged(sensorData));
|
||||
}
|
||||
}
|
||||
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
|
||||
}
|
||||
};
|
||||
|
||||
AbstractBluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = List.of(serviceUUUID);
|
||||
BluetoothConnectionManager(SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
|
||||
this.observer = observer;
|
||||
this.sensorHandler = sensorHandler;
|
||||
}
|
||||
|
||||
AbstractBluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = serviceUUUID;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
|
||||
synchronized void connect(Context context, Handler handler, @NonNull BluetoothDevice device) {
|
||||
if (bluetoothGatt != null) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
return;
|
||||
@@ -150,20 +137,15 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
|
||||
|
||||
Log.d(TAG, "Connecting to: " + device);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
|
||||
} else {
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback);
|
||||
}
|
||||
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
|
||||
observer.onChanged(sensorData);
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||
|
||||
observer.onConnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
|
||||
}
|
||||
|
||||
private synchronized void clearData() {
|
||||
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
|
||||
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
|
||||
}
|
||||
|
||||
|
||||
synchronized void disconnect() {
|
||||
if (bluetoothGatt == null) {
|
||||
Log.w(TAG, "Cannot disconnect if not connected.");
|
||||
@@ -181,22 +163,4 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
|
||||
|
||||
return address.equals(bluetoothGatt.getDevice().getAddress());
|
||||
}
|
||||
|
||||
protected abstract SensorData<DataType> createEmptySensorData(String address);
|
||||
|
||||
/**
|
||||
* @return null if data could not be parsed.
|
||||
*/
|
||||
protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic);
|
||||
|
||||
@Deprecated //TODO REMOVE
|
||||
interface SensorDataObserver {
|
||||
|
||||
void onChanged(SensorData<?> sensorData);
|
||||
|
||||
void onDisconnecting(SensorData<?> sensorData);
|
||||
|
||||
@NonNull
|
||||
Handler getHandler();
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
|
||||
public class BluetoothConnectionManagerCyclingCadence extends AbstractBluetoothConnectionManager<Cadence> {
|
||||
|
||||
private static final String TAG = BluetoothConnectionManagerCyclingCadence.class.getSimpleName();
|
||||
|
||||
BluetoothConnectionManagerCyclingCadence(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_CADENCE, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingCadence createEmptySensorData(String address) {
|
||||
return new SensorDataCyclingCadence(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingCadence parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
|
||||
//TODO Implement to ServiceMeasurement.parse()?
|
||||
if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_POWER)) {
|
||||
SensorDataCyclingPower.Data data = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
|
||||
if (data!= null) {
|
||||
return data.cadence();
|
||||
}
|
||||
} else if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_SPEED_CADENCE)) {
|
||||
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cadenceAndSpeed.getCadence() != null) {
|
||||
return cadenceAndSpeed.getCadence();
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "Don't know how to decode this payload.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
|
||||
|
||||
public class BluetoothConnectionManagerCyclingDistanceSpeed extends AbstractBluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> {
|
||||
|
||||
BluetoothConnectionManagerCyclingDistanceSpeed(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingDistanceSpeed createEmptySensorData(String address) {
|
||||
return new SensorDataCyclingDistanceSpeed(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingDistanceSpeed parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cadenceAndSpeed.getDistanceSpeed() != null) {
|
||||
return cadenceAndSpeed.getDistanceSpeed();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
|
||||
public class BluetoothConnectionManagerCyclingPower extends AbstractBluetoothConnectionManager<Power> {
|
||||
|
||||
BluetoothConnectionManagerCyclingPower(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_POWER, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingPower createEmptySensorData(String address) {
|
||||
return new SensorDataCyclingPower(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingPower parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCyclingPower.Data cyclingPower = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic);
|
||||
|
||||
return cyclingPower != null ? cyclingPower.power() : null;
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
|
||||
public class BluetoothConnectionManagerHeartRate extends AbstractBluetoothConnectionManager<HeartRate> {
|
||||
|
||||
BluetoothConnectionManagerHeartRate(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.HEARTRATE, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataHeartRate createEmptySensorData(String address) {
|
||||
return new SensorDataHeartRate(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataHeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
public class BluetoothConnectionRunningSpeedAndCadence extends AbstractBluetoothConnectionManager<SensorDataRunning.Data> {
|
||||
|
||||
BluetoothConnectionRunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataRunning createEmptySensorData(String address) {
|
||||
return new SensorDataRunning(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataRunning parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
|
||||
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(BAROMETRIC_PRESSURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Aggregator<?, ?> createEmptySensorData(String address) {
|
||||
return null; //TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
//TODO
|
||||
}
|
||||
|
||||
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] raw = characteristic.getValue();
|
||||
|
||||
if (raw.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
|
||||
return AtmosphericPressure.ofPA(pressure / 10f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
|
||||
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
|
||||
|
||||
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
|
||||
BluetoothHandlerManagerCyclingPower.CYCLING_POWER,
|
||||
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return CYCLING_CADENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingCadence createEmptySensorData(String address) {
|
||||
return new AggregatorCyclingCadence(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) {
|
||||
BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic);
|
||||
if (data != null && data.crank() != null) {
|
||||
observer.onChange(new Raw<>(data.crank()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) {
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
|
||||
if (data != null && data.second != null) {
|
||||
observer.onChange(new Raw<>(data.second));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Don't know how to decode this payload.");
|
||||
}
|
||||
|
||||
public record CrankData(
|
||||
long crankRevolutionsCount, // UINT32
|
||||
int crankRevolutionsTime // UINT16; 1/1024s
|
||||
) {}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface {
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181600001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(CYCLING_SPEED_CADENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address) {
|
||||
return new AggregatorCyclingDistanceSpeed(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (data.first != null) {
|
||||
observer.onChange(new Raw<>(data.first));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public static Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasWheel = (flags & 0x01) > 0;
|
||||
boolean hasCrank = (flags & 0x02) > 0;
|
||||
|
||||
int index = 1;
|
||||
WheelData wheelData = null;
|
||||
if (hasWheel && valueLength - index >= 6) {
|
||||
long wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
|
||||
index += 4;
|
||||
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
wheelData = new WheelData(wheelTotalRevolutionCount, wheelTime);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
BluetoothHandlerCyclingCadence.CrankData crankData = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new Pair<>(wheelData, crankData);
|
||||
}
|
||||
|
||||
public record WheelData(
|
||||
|
||||
long wheelRevolutionsCount, // UINT32
|
||||
|
||||
int wheelRevolutionsTime // UINT16; 1/1024s
|
||||
) {}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterface {
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
|
||||
new UUID(0x181800001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(CYCLING_POWER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingPower createEmptySensorData(String address) {
|
||||
return new AggregatorCyclingPower(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Data cyclingPower = parseCyclingPower(characteristic);
|
||||
|
||||
if (cyclingPower != null) {
|
||||
observer.onChange(new Raw<>(cyclingPower));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public static Data parseCyclingPower(BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
|
||||
int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
|
||||
boolean hasPedalPowerBalance = (flags1 & 0x01) > 0;
|
||||
boolean hasAccumulatedTorque = (flags1 & 0x04) > 0;
|
||||
boolean hasWheel = (flags1 & 16) > 0;
|
||||
boolean hasCrank = (flags1 & 32) > 0;
|
||||
|
||||
Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index);
|
||||
index += 2;
|
||||
|
||||
if (hasPedalPowerBalance) {
|
||||
index += 1;
|
||||
}
|
||||
if (hasAccumulatedTorque) {
|
||||
index += 2;
|
||||
}
|
||||
if (hasWheel) {
|
||||
index += 2 + 2;
|
||||
}
|
||||
|
||||
BluetoothHandlerCyclingCadence.CrankData crankData = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
|
||||
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new Data(Power.of(instantaneousPower), crankData);
|
||||
}
|
||||
|
||||
public record Data(Power power, BluetoothHandlerCyclingCadence.CrankData crank) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface {
|
||||
|
||||
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
|
||||
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
// Used for device discovery in preferences
|
||||
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = List.of(
|
||||
HEARTRATE,
|
||||
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
|
||||
new ServiceMeasurementUUID(
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
|
||||
HEARTRATE.measurementUUID()
|
||||
)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return HEART_RATE_SUPPORTING_DEVICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorHeartRate createEmptySensorData(String address) {
|
||||
return new AggregatorHeartRate(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
HeartRate heartRate = parseHeartRate(characteristic);
|
||||
|
||||
if (heartRate != null) {
|
||||
observer.onChange(new Raw<>(heartRate));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
|
||||
byte[] raw = characteristic.getValue();
|
||||
if (raw.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
|
||||
if (formatUINT16 && raw.length >= 3) {
|
||||
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1));
|
||||
}
|
||||
if (!formatUINT16 && raw.length >= 2) {
|
||||
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInterface {
|
||||
|
||||
|
||||
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181400001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A5300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(RUNNING_SPEED_CADENCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorRunning createEmptySensorData(String address) {
|
||||
return new AggregatorRunning(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Data data = parseRunningSpeedAndCadence(sensorName, characteristic);
|
||||
observer.onChange(new Raw<>(data));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static Data parseRunningSpeedAndCadence(String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasStrideLength = (flags & 0x01) > 0;
|
||||
boolean hasTotalDistance = (flags & 0x02) > 0;
|
||||
boolean hasStatus = (flags & 0x03) > 0; // walking vs running
|
||||
|
||||
Speed speed = null;
|
||||
Cadence cadence = null;
|
||||
Distance totalDistance = null;
|
||||
|
||||
int index = 1;
|
||||
if (valueLength - index >= 2) {
|
||||
speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f);
|
||||
}
|
||||
|
||||
index = 3;
|
||||
if (valueLength - index >= 1) {
|
||||
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
|
||||
|
||||
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
|
||||
if (sensorName != null && sensorName.startsWith("TICKR X")) {
|
||||
cadence = Cadence.of(cadence.getRPM() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
index = 4;
|
||||
if (hasStrideLength && valueLength - index >= 2) {
|
||||
Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index));
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (hasTotalDistance && valueLength - index >= 4) {
|
||||
totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index));
|
||||
}
|
||||
|
||||
return new Data(speed, cadence, totalDistance);
|
||||
}
|
||||
|
||||
public record Data(Speed speed, Cadence cadence, Distance totalDistance) {}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
@@ -47,29 +46,34 @@ import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBluetoothConnectionManager.SensorDataObserver, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
|
||||
|
||||
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
|
||||
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(50); //TODO
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private final Context context;
|
||||
private final Handler handler;
|
||||
private final SensorManager.SensorDataChangedObserver observer;
|
||||
private boolean started = false;
|
||||
|
||||
private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this);
|
||||
private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this);
|
||||
private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this);
|
||||
private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this);
|
||||
private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this);
|
||||
private final BluetoothConnectionManager heartRate;
|
||||
private final BluetoothConnectionManager cyclingCadence;
|
||||
private final BluetoothConnectionManager cyclingSpeed;
|
||||
private final BluetoothConnectionManager cyclingPower;
|
||||
private final BluetoothConnectionManager runningSpeedAndCadence;
|
||||
|
||||
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
this.observer = observer;
|
||||
bluetoothAdapter = BluetoothUtils.getAdapter(context);
|
||||
|
||||
this.heartRate = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerHeartRate());
|
||||
this.cyclingCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
|
||||
this.cyclingSpeed = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
|
||||
this.cyclingPower = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerCyclingPower());
|
||||
this.runningSpeedAndCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerRunningSpeedAndCadence());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +99,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
private synchronized void connect(AbstractBluetoothConnectionManager<?> connectionManager, String address) {
|
||||
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
|
||||
if (!isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
return;
|
||||
@@ -120,28 +124,12 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
|
||||
Log.i(TAG, "Connecting to bluetooth address: " + address);
|
||||
try {
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
||||
connectionManager.connect(context, device);
|
||||
connectionManager.connect(context, handler, device);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "Unable to get remote device for: " + address, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onChanged(SensorData<?> sensorData) {
|
||||
observer.onChange(sensorData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnecting(SensorData<?> sensorData) {
|
||||
observer.onDisconnect(sensorData);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Handler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) {
|
||||
if (!started) return;
|
||||
|
||||
@@ -21,23 +21,9 @@ import android.bluetooth.BluetoothManager;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bluetooth devices.
|
||||
@@ -53,52 +39,9 @@ public class BluetoothUtils {
|
||||
new UUID(0x2A1900001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
|
||||
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
// Used for device discovery in preferences
|
||||
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = List.of(
|
||||
HEARTRATE,
|
||||
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
|
||||
new ServiceMeasurementUUID(
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
|
||||
HEARTRATE.measurementUUID()
|
||||
)
|
||||
);
|
||||
|
||||
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
|
||||
new UUID(0x181800001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181600001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
|
||||
CYCLING_POWER,
|
||||
CYCLING_SPEED_CADENCE
|
||||
);
|
||||
|
||||
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181400001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A5300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
private static final String TAG = BluetoothUtils.class.getSimpleName();
|
||||
|
||||
private BluetoothUtils() {
|
||||
}
|
||||
|
||||
public static BluetoothAdapter getAdapter(Context context) {
|
||||
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (bluetoothManager == null) {
|
||||
@@ -123,152 +66,4 @@ public class BluetoothUtils {
|
||||
final int batteryLevel = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
|
||||
return BatteryLevel.of(batteryLevel);
|
||||
}
|
||||
|
||||
public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
|
||||
byte[] raw = characteristic.getValue();
|
||||
if (raw.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
|
||||
if (formatUINT16 && raw.length >= 3) {
|
||||
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1));
|
||||
}
|
||||
if (!formatUINT16 && raw.length >= 2) {
|
||||
return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] raw = characteristic.getValue();
|
||||
|
||||
if (raw.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
|
||||
return AtmosphericPressure.ofPA(pressure / 10f);
|
||||
}
|
||||
|
||||
public static SensorDataCyclingPower.Data parseCyclingPower(String address, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
|
||||
int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++);
|
||||
boolean hasPedalPowerBalance = (flags1 & 0x01) > 0;
|
||||
boolean hasAccumulatedTorque = (flags1 & 0x04) > 0;
|
||||
boolean hasWheel = (flags1 & 16) > 0;
|
||||
boolean hasCrank = (flags1 & 32) > 0;
|
||||
|
||||
Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index);
|
||||
index += 2;
|
||||
|
||||
if (hasPedalPowerBalance) {
|
||||
index += 1;
|
||||
}
|
||||
if (hasAccumulatedTorque) {
|
||||
index += 2;
|
||||
}
|
||||
if (hasWheel) {
|
||||
index += 2 + 2;
|
||||
}
|
||||
|
||||
SensorDataCyclingCadence cadence = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
|
||||
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
|
||||
}
|
||||
|
||||
|
||||
return new SensorDataCyclingPower.Data(new SensorDataCyclingPower(sensorName, address, Power.of(instantaneousPower)), cadence);
|
||||
}
|
||||
|
||||
public static SensorDataCyclingCadenceAndDistanceSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasWheel = (flags & 0x01) > 0;
|
||||
boolean hasCrank = (flags & 0x02) > 0;
|
||||
|
||||
int index = 1;
|
||||
SensorDataCyclingDistanceSpeed speed = null;
|
||||
if (hasWheel && valueLength - index >= 6) {
|
||||
int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
|
||||
index += 4;
|
||||
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
speed = new SensorDataCyclingDistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
SensorDataCyclingCadence cadence = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new SensorDataCyclingCadenceAndDistanceSpeed(address, sensorName, cadence, speed);
|
||||
}
|
||||
|
||||
public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasStrideLength = (flags & 0x01) > 0;
|
||||
boolean hasTotalDistance = (flags & 0x02) > 0;
|
||||
boolean hasStatus = (flags & 0x03) > 0; // walking vs running
|
||||
|
||||
Speed speed = null;
|
||||
Cadence cadence = null;
|
||||
Distance totalDistance = null;
|
||||
|
||||
int index = 1;
|
||||
if (valueLength - index >= 2) {
|
||||
speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f);
|
||||
}
|
||||
|
||||
index = 3;
|
||||
if (valueLength - index >= 1) {
|
||||
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
|
||||
|
||||
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
|
||||
if (sensorName != null && sensorName.startsWith("TICKR X")) {
|
||||
cadence = Cadence.of(cadence.getRPM() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
index = 4;
|
||||
if (hasStrideLength && valueLength - index >= 2) {
|
||||
Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index));
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (hasTotalDistance && valueLength - index >= 4) {
|
||||
totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index));
|
||||
}
|
||||
|
||||
return new SensorDataRunning(address, sensorName, speed, cadence, totalDistance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
*/
|
||||
public class GainManager implements SensorConnector {
|
||||
|
||||
private static final String TAG = GainManager.class.getSimpleName();
|
||||
|
||||
private final BarometerInternal driver;
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver listener;
|
||||
|
||||
public GainManager(SensorManager.SensorDataChangedObserver listener) {
|
||||
this.listener = listener;
|
||||
driver = new BarometerInternal();
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
driver.connect(context, handler, this);
|
||||
|
||||
if (driver.isConnected()) {
|
||||
listener.onConnect(new AggregatorBarometer("internal"));
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(Context context) {
|
||||
Log.d(TAG, "Stop");
|
||||
|
||||
driver.disconnect(context);
|
||||
listener.onDisconnect(new AggregatorBarometer("internal"));
|
||||
}
|
||||
|
||||
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
|
||||
listener.onChange(new Raw<>(currentSensorValue));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.handlers.GPSManager;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
@@ -27,13 +28,19 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
|
||||
|
||||
@Override
|
||||
public void onChange(SensorData<?> sensorData) {
|
||||
sensorDataSet.set(sensorData);
|
||||
public void onConnect(Aggregator<?, ?> sensorData) {
|
||||
sensorDataSet.add(sensorData);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(SensorData<?> sensorData) {
|
||||
public void onChange(Raw<?> sensorData) {
|
||||
sensorDataSet.update(sensorData);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Aggregator<?, ?> sensorData) {
|
||||
sensorDataSet.remove(sensorData);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
}
|
||||
@@ -41,7 +48,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
private BluetoothRemoteSensorManager bluetoothSensorManager;
|
||||
|
||||
private AltitudeSumManager altitudeSumManager;
|
||||
private GainManager altitudeSumManager;
|
||||
|
||||
private GPSManager gpsManager;
|
||||
|
||||
@@ -51,7 +58,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
gpsManager = new GPSManager(observer); //TODO Pass listener
|
||||
altitudeSumManager = new AltitudeSumManager();
|
||||
altitudeSumManager = new GainManager(listener);
|
||||
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
|
||||
|
||||
onSharedPreferenceChanged(null, null);
|
||||
@@ -75,7 +82,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public SensorDataSet fill(TrackPoint trackPoint) {
|
||||
altitudeSumManager.fill(trackPoint);
|
||||
sensorDataSet.fillTrackPoint(trackPoint);
|
||||
return new SensorDataSet(sensorDataSet);
|
||||
}
|
||||
@@ -86,13 +92,12 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
return;
|
||||
}
|
||||
sensorDataSet.reset();
|
||||
altitudeSumManager.reset();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public BluetoothRemoteSensorManager getBluetoothSensorManager() {
|
||||
return bluetoothSensorManager;
|
||||
public void onChanged(Raw<?> data) {
|
||||
listener.onChange(data);
|
||||
}
|
||||
|
||||
public GPSManager getGpsManager() {
|
||||
@@ -101,13 +106,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public AltitudeSumManager getAltitudeSumManager() {
|
||||
public GainManager getAltitudeSumManager() {
|
||||
return altitudeSumManager;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) {
|
||||
public void setAltitudeSumManager(GainManager altitudeSumManager) {
|
||||
this.altitudeSumManager = altitudeSumManager;
|
||||
}
|
||||
|
||||
@@ -120,8 +125,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public interface SensorDataChangedObserver {
|
||||
void onChange(SensorData<?> sensorData);
|
||||
|
||||
void onDisconnect(SensorData<?> sensorData);
|
||||
void onConnect(Aggregator<?, ?> sensorData);
|
||||
void onChange(Raw<?> sensorData);
|
||||
|
||||
void onDisconnect(Aggregator<?, ?> sensorData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,44 +11,40 @@ import android.util.Log;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
|
||||
import de.dennisguse.opentracks.sensors.GainManager;
|
||||
|
||||
public class BarometerInternal {
|
||||
public class BarometerInternal implements SensorEventListener {
|
||||
|
||||
private static final String TAG = BarometerInternal.class.getSimpleName();
|
||||
|
||||
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
|
||||
|
||||
private AltitudeSumManager observer;
|
||||
private GainManager observer;
|
||||
|
||||
private final SensorEventListener listener = new SensorEventListener() {
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Not connected to sensor, cannot process data.");
|
||||
return;
|
||||
}
|
||||
|
||||
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Not connected to sensor, cannot process data.");
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
|
||||
}
|
||||
};
|
||||
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
|
||||
}
|
||||
|
||||
public void connect(Context context, Handler handler, AltitudeSumManager observer) {
|
||||
this.observer = observer;
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
|
||||
}
|
||||
|
||||
public void connect(Context context, Handler handler, GainManager observer) {
|
||||
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
||||
if (pressureSensor == null) {
|
||||
Log.w(TAG, "No pressure sensor available.");
|
||||
this.observer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sensorManager.registerListener(listener, pressureSensor, SAMPLING_PERIOD, handler)) {
|
||||
if (sensorManager.registerListener(this, pressureSensor, SAMPLING_PERIOD, handler)) {
|
||||
this.observer = observer;
|
||||
return;
|
||||
}
|
||||
@@ -58,7 +54,8 @@ public class BarometerInternal {
|
||||
|
||||
public void disconnect(Context context) {
|
||||
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
sensorManager.unregisterListener(listener);
|
||||
sensorManager.unregisterListener(this);
|
||||
observer = null;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
|
||||
+22
-19
@@ -1,48 +1,48 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
|
||||
|
||||
public abstract class SensorData<T> {
|
||||
public abstract class Aggregator<Input extends Record, Output> {
|
||||
|
||||
protected T value;
|
||||
protected Raw<Input> previous;
|
||||
|
||||
protected Output value;
|
||||
|
||||
private final String sensorAddress;
|
||||
private final String sensorName;
|
||||
|
||||
private final Instant time;
|
||||
|
||||
SensorData(String sensorAddress) {
|
||||
Aggregator(String sensorAddress) {
|
||||
this(sensorAddress, null);
|
||||
}
|
||||
|
||||
SensorData(String sensorAddress, String sensorName) {
|
||||
this(sensorAddress, sensorName, Instant.now());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SensorData(String sensorAddress, String sensorName, Instant time) {
|
||||
Aggregator(String sensorAddress, String sensorName) {
|
||||
this.sensorAddress = sensorAddress;
|
||||
this.sensorName = sensorName;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getSensorNameOrAddress() {
|
||||
return sensorName != null ? sensorName : sensorAddress;
|
||||
}
|
||||
|
||||
public final void add(Raw<Input> current) {
|
||||
computeValue(current);
|
||||
previous = current;
|
||||
}
|
||||
|
||||
protected abstract void computeValue(Raw<Input> current);
|
||||
|
||||
public boolean hasValue() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract T getNoneValue();
|
||||
protected abstract Output getNoneValue();
|
||||
|
||||
public T getValue() {
|
||||
public Output getValue() {
|
||||
if (!hasValue()) {
|
||||
return null;
|
||||
}
|
||||
@@ -55,20 +55,23 @@ public abstract class SensorData<T> {
|
||||
/**
|
||||
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
|
||||
*/
|
||||
public void reset() {
|
||||
}
|
||||
public void reset() {};
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*/
|
||||
private boolean isRecent() {
|
||||
if (previous == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Instant.now()
|
||||
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
|
||||
.isBefore(previous.time().plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "sensorAddress='" + sensorAddress;
|
||||
return "sensorAddress=" + sensorAddress + " data=" + value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.PressureSensorUtils;
|
||||
|
||||
public class AggregatorBarometer extends Aggregator<AtmosphericPressure, AltitudeGainLoss> {
|
||||
|
||||
private AtmosphericPressure lastAcceptedSensorValue;
|
||||
|
||||
public AggregatorBarometer(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<AtmosphericPressure> current) {
|
||||
if (previous == null) {
|
||||
lastAcceptedSensorValue = current.value();
|
||||
value = getNoneValue();
|
||||
return;
|
||||
}
|
||||
|
||||
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, previous.value(), current.value());
|
||||
if (altitudeChange != null) {
|
||||
value = new AltitudeGainLoss(value.gain_m() + altitudeChange.getAltitudeGain_m(), value.loss_m() + altitudeChange.getAltitudeLoss_m());
|
||||
|
||||
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AltitudeGainLoss getNoneValue() {
|
||||
return new AltitudeGainLoss(0f, 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
value = getNoneValue();
|
||||
}
|
||||
|
||||
public record Data(Altitude gain, Altitude loss) {}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCyclingCadence.CrankData, Cadence> {
|
||||
|
||||
private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
|
||||
|
||||
public AggregatorCyclingCadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
}
|
||||
|
||||
public AggregatorCyclingCadence(String sensorAddress, String sensorName) {
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<BluetoothHandlerCyclingCadence.CrankData> current) {
|
||||
if (previous != null) {
|
||||
float timeDiff_ms = UintUtils.diff(current.value().crankRevolutionsTime(), previous.value().crankRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000;
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
if (timeDiff.isZero() || timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
|
||||
if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) {
|
||||
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
|
||||
return;
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
value = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Cadence getNoneValue() {
|
||||
return Cadence.of(0);
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerCyclingDistanceSpeed.WheelData, AggregatorCyclingDistanceSpeed.Data> {
|
||||
|
||||
private final String TAG = AggregatorCyclingDistanceSpeed.class.getSimpleName();
|
||||
|
||||
private Distance wheelCircumference;
|
||||
|
||||
public AggregatorCyclingDistanceSpeed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
}
|
||||
|
||||
public AggregatorCyclingDistanceSpeed(String sensorAddress, String sensorName) {
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData> current) {
|
||||
if (previous != null) {
|
||||
float timeDiff_ms = UintUtils.diff(current.value().wheelRevolutionsTime(), previous.value().wheelRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000;
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
if (timeDiff.isZero() || timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
|
||||
value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.value().wheelRevolutionsCount() < previous.value().wheelRevolutionsCount()) {
|
||||
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
|
||||
return;
|
||||
}
|
||||
long wheelDiff = UintUtils.diff(current.value().wheelRevolutionsCount(), previous.value().wheelRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
|
||||
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||
Distance distanceOverall = distance;
|
||||
if (value != null) {
|
||||
distanceOverall = distance.plus(value.distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
value = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
if (value != null) {
|
||||
value = new Data(value.distance, Distance.of(0), value.speed);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Data getNoneValue() {
|
||||
if (value != null) {
|
||||
return new Data(value.distance, value.distanceOverall, Speed.zero());
|
||||
} else {
|
||||
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
|
||||
}
|
||||
}
|
||||
|
||||
public void setWheelCircumference(Distance wheelCircumference) {
|
||||
this.wheelCircumference = wheelCircumference;
|
||||
}
|
||||
|
||||
public record Data(Distance distance, Distance distanceOverall, Speed speed) {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
|
||||
public class AggregatorCyclingPower extends Aggregator<Power, Power> {
|
||||
|
||||
public AggregatorCyclingPower(String address) {
|
||||
super(address);
|
||||
}
|
||||
|
||||
public AggregatorCyclingPower(String name, String address) {
|
||||
super(name, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeValue(Raw<Power> current) {
|
||||
this.value = current.value();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Power getNoneValue() {
|
||||
return Power.of(0f);
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -4,21 +4,19 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
|
||||
public class SensorDataHeartRate extends SensorData<HeartRate> {
|
||||
public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
|
||||
|
||||
public SensorDataHeartRate(String address) {
|
||||
public AggregatorHeartRate(String address) {
|
||||
super(address);
|
||||
}
|
||||
|
||||
public SensorDataHeartRate(String name, String address, @NonNull HeartRate heartRate) {
|
||||
public AggregatorHeartRate(String name, String address) {
|
||||
super(name, address);
|
||||
this.value = heartRate;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " heart=" + value;
|
||||
protected void computeValue(Raw<HeartRate> current) {
|
||||
this.value = current.value();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
|
||||
/**
|
||||
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
|
||||
*/
|
||||
public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningSpeedAndCadence.Data, AggregatorRunning.Data> {
|
||||
|
||||
private static final String TAG = AggregatorRunning.class.getSimpleName();
|
||||
|
||||
public AggregatorRunning(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
}
|
||||
|
||||
public AggregatorRunning(String sensorAddress, String sensorName) {
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeValue(Raw<BluetoothHandlerRunningSpeedAndCadence.Data> current) {
|
||||
if (previous != null) {
|
||||
|
||||
Distance distance = null;
|
||||
if (previous.value().totalDistance() != null && current.value().totalDistance() != null) {
|
||||
distance = current.value().totalDistance().minus(previous.value().totalDistance());
|
||||
if (value != null) {
|
||||
distance = distance.plus(value.distance);
|
||||
}
|
||||
}
|
||||
|
||||
value = new Data(current.value().speed(), current.value().cadence(), distance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
if (value != null) {
|
||||
value = new Data(value.speed, value.cadence, Distance.of(0));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Data getNoneValue() {
|
||||
if (value != null) {
|
||||
return new Data(Speed.zero(), Cadence.of(0f), value.distance);
|
||||
} else {
|
||||
return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0));
|
||||
}
|
||||
}
|
||||
|
||||
public record Data(Speed speed, Cadence cadence, @NonNull Distance distance) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record Raw<T extends Record>(
|
||||
@NonNull T value,
|
||||
|
||||
@NonNull Instant time
|
||||
) {
|
||||
public Raw(@NonNull T value) {
|
||||
this(value, Instant.now()); //TODO We should be using the MonotonicClock
|
||||
}
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
public class SensorDataCyclingCadence extends SensorData<Cadence> {
|
||||
|
||||
private final String TAG = SensorDataCyclingCadence.class.getSimpleName();
|
||||
|
||||
private final Long crankRevolutionsCount; // UINT32
|
||||
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||
|
||||
public SensorDataCyclingCadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.crankRevolutionsCount = null;
|
||||
this.crankRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.crankRevolutionsCount = crankRevolutionsCount;
|
||||
this.crankRevolutionsTime = crankRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return crankRevolutionsCount != null && crankRevolutionsTime != null;
|
||||
}
|
||||
|
||||
public long getCrankRevolutionsCount() {
|
||||
return crankRevolutionsCount;
|
||||
}
|
||||
|
||||
public int getCrankRevolutionsTime() {
|
||||
return crankRevolutionsTime;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Cadence getNoneValue() {
|
||||
return Cadence.of(0);
|
||||
}
|
||||
|
||||
public void compute(SensorDataCyclingCadence previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
if (timeDiff.isZero() || timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268
|
||||
if (crankRevolutionsCount < previous.crankRevolutionsCount) {
|
||||
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
|
||||
return;
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
value = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof SensorDataCyclingCadence comp)) return false;
|
||||
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
public class SensorDataCyclingCadenceAndDistanceSpeed extends SensorData<Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed>> {
|
||||
|
||||
public SensorDataCyclingCadenceAndDistanceSpeed(String sensorAddress, String sensorName, @Nullable SensorDataCyclingCadence cadence, @Nullable SensorDataCyclingDistanceSpeed distanceSpeed) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.value = new Pair<>(cadence, distanceSpeed);
|
||||
}
|
||||
|
||||
public SensorDataCyclingCadence getCadence() {
|
||||
return this.value != null ? this.value.first : null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingDistanceSpeed getDistanceSpeed() {
|
||||
return this.value != null ? this.value.second : null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Pair<SensorDataCyclingCadence, SensorDataCyclingDistanceSpeed> getNoneValue() {
|
||||
return new Pair<>(null, null);
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
public class SensorDataCyclingDistanceSpeed extends SensorData<SensorDataCyclingDistanceSpeed.Data> {
|
||||
|
||||
private final String TAG = SensorDataCyclingDistanceSpeed.class.getSimpleName();
|
||||
|
||||
private final Long wheelRevolutionsCount; // UINT32
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
|
||||
public SensorDataCyclingDistanceSpeed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.wheelRevolutionsCount = null;
|
||||
this.wheelRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingDistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.wheelRevolutionsCount = wheelRevolutionsCount;
|
||||
this.wheelRevolutionsTime = wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
|
||||
}
|
||||
|
||||
public long getWheelRevolutionsCount() {
|
||||
return wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
public int getWheelRevolutionsTime() {
|
||||
return wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Data getNoneValue() {
|
||||
if (value != null) {
|
||||
return new Data(value.distance, value.distanceOverall, Speed.zero());
|
||||
} else {
|
||||
return new Data(Distance.of(0), Distance.of(0), Speed.zero());
|
||||
}
|
||||
}
|
||||
|
||||
public void compute(SensorDataCyclingDistanceSpeed previous, Distance wheelCircumference) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000;
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
if (timeDiff.isZero() || timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute speed.");
|
||||
value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wheelRevolutionsCount < previous.wheelRevolutionsCount) {
|
||||
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
|
||||
return;
|
||||
}
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
|
||||
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||
Distance distanceOverall = distance;
|
||||
if (previous.hasValue()) {
|
||||
distanceOverall = distance.plus(previous.getValue().distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
value = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
if (value != null) {
|
||||
value = new Data(value.distance, Distance.of(0), value.speed);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof SensorDataCyclingDistanceSpeed comp)) return false;
|
||||
|
||||
if (!(hasData() && comp.hasData())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
private final Distance distance;
|
||||
private final Distance distanceOverall;
|
||||
private final Speed speed;
|
||||
|
||||
private Data(Distance distance, Distance distanceOverall, Speed speed) {
|
||||
this.distance = distance;
|
||||
this.distanceOverall = distanceOverall;
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public Distance getDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
public Distance getDistanceOverall() {
|
||||
return distanceOverall;
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Data{" +
|
||||
"distance=" + getDistance() +
|
||||
", distance_overall=" + getDistanceOverall() +
|
||||
", speed=" + getSpeed() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
|
||||
public class SensorDataCyclingPower extends SensorData<Power> {
|
||||
|
||||
public SensorDataCyclingPower(String address) {
|
||||
super(address);
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower(String name, String address, Power power) {
|
||||
super(name, address);
|
||||
this.value = power;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Power getNoneValue() {
|
||||
return Power.of(0f);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " data=" + value;
|
||||
}
|
||||
|
||||
public record Data(SensorDataCyclingPower power, SensorDataCyclingCadence cadence) {
|
||||
public Data(SensorDataCyclingPower power, @Nullable SensorDataCyclingCadence cadence) {
|
||||
this.power = power;
|
||||
this.cadence = cadence;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Data{" +
|
||||
"power=" + power +
|
||||
", cadence=" + cadence +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
|
||||
/**
|
||||
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
|
||||
*/
|
||||
public final class SensorDataRunning extends SensorData<SensorDataRunning.Data> {
|
||||
|
||||
private static final String TAG = SensorDataRunning.class.getSimpleName();
|
||||
|
||||
private final Speed speed;
|
||||
|
||||
private final Cadence cadence;
|
||||
|
||||
private final Distance totalDistance;
|
||||
|
||||
public SensorDataRunning(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.speed = null;
|
||||
this.cadence = null;
|
||||
this.totalDistance = null;
|
||||
}
|
||||
|
||||
public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Cadence cadence, Distance totalDistance) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.speed = speed;
|
||||
this.cadence = cadence;
|
||||
this.totalDistance = totalDistance;
|
||||
}
|
||||
|
||||
private boolean hasTotalDistance() {
|
||||
return totalDistance != null;
|
||||
}
|
||||
|
||||
|
||||
public Cadence getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Distance getTotalDistance() {
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Data getNoneValue() {
|
||||
if (value != null) {
|
||||
return new Data(Speed.zero(), Cadence.of(0f), value.distance);
|
||||
} else {
|
||||
return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0));
|
||||
}
|
||||
}
|
||||
|
||||
public void compute(SensorDataRunning previous) {
|
||||
if (speed != null && hasTotalDistance()) {
|
||||
Distance overallDistance = null;
|
||||
if (previous != null && previous.hasTotalDistance()) {
|
||||
overallDistance = this.totalDistance.minus(previous.totalDistance);
|
||||
if (previous.hasValue() && previous.getValue().distance() != null) {
|
||||
overallDistance = overallDistance.plus(previous.getValue().distance());
|
||||
}
|
||||
}
|
||||
|
||||
value = new Data(speed, cadence, overallDistance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
if (value != null) {
|
||||
value = new Data(value.speed, value.cadence, Distance.of(0));
|
||||
}
|
||||
}
|
||||
|
||||
public record Data(Speed speed, Cadence cadence, @Nullable Distance distance) {}
|
||||
}
|
||||
|
||||
@@ -5,27 +5,41 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public final class SensorDataSet {
|
||||
|
||||
private static final String TAG = SensorDataSet.class.getSimpleName();
|
||||
|
||||
private SensorDataHeartRate heartRate;
|
||||
@VisibleForTesting
|
||||
public AggregatorHeartRate heartRate;
|
||||
|
||||
private SensorDataCyclingCadence cyclingCadence;
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingCadence cyclingCadence;
|
||||
|
||||
private SensorDataCyclingDistanceSpeed cyclingDistanceSpeed;
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
|
||||
|
||||
private SensorDataCyclingPower cyclingPower;
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingPower cyclingPower;
|
||||
|
||||
private SensorDataRunning runningDistanceSpeedCadence;
|
||||
@VisibleForTesting
|
||||
public AggregatorRunning runningDistanceSpeedCadence;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorBarometer barometer;
|
||||
|
||||
public SensorDataSet() {
|
||||
}
|
||||
@@ -36,6 +50,7 @@ public final class SensorDataSet {
|
||||
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
|
||||
this.cyclingPower = toCopy.cyclingPower;
|
||||
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
|
||||
this.barometer = toCopy.barometer;
|
||||
}
|
||||
|
||||
public Pair<HeartRate, String> getHeartRate() {
|
||||
@@ -51,55 +66,79 @@ public final class SensorDataSet {
|
||||
return new Pair<>(cyclingCadence.getValue(), cyclingCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null) {
|
||||
return new Pair<>(runningDistanceSpeedCadence.getCadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.value.cadence() != null) {
|
||||
return new Pair<>(runningDistanceSpeedCadence.value.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Pair<Speed, String> getSpeed() {
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().getSpeed() != null) {
|
||||
return new Pair<>(cyclingDistanceSpeed.getValue().getSpeed(), cyclingDistanceSpeed.getSensorNameOrAddress());
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().speed() != null) {
|
||||
return new Pair<>(cyclingDistanceSpeed.getValue().speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue().speed() != null) {
|
||||
return new Pair<>(runningDistanceSpeedCadence.getSpeed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
return new Pair<>(runningDistanceSpeedCadence.value.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingCadence getCyclingCadence() {
|
||||
return cyclingCadence;
|
||||
}
|
||||
|
||||
public SensorDataCyclingDistanceSpeed getCyclingDistanceSpeed() {
|
||||
return cyclingDistanceSpeed;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower getCyclingPower() {
|
||||
public AggregatorCyclingPower getCyclingPower() {
|
||||
return cyclingPower;
|
||||
}
|
||||
|
||||
public SensorDataRunning getRunningDistanceSpeedCadence() {
|
||||
return runningDistanceSpeedCadence;
|
||||
}
|
||||
|
||||
public void set(SensorData<?> data) {
|
||||
public void add(@NonNull Aggregator<?, ?> data) {
|
||||
set(data, data);
|
||||
}
|
||||
|
||||
public void remove(SensorData<?> type) {
|
||||
public void update(@NonNull Raw<?> data) {
|
||||
Record value = data.value();
|
||||
|
||||
if (value instanceof HeartRate) {
|
||||
this.heartRate.add((Raw<HeartRate>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
|
||||
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData ) {
|
||||
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
|
||||
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
|
||||
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
|
||||
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerManagerCyclingPower.Data) {
|
||||
this.cyclingPower.add((Raw<Power>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof AtmosphericPressure) {
|
||||
this.barometer.add((Raw<AtmosphericPressure>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
public void remove(@NonNull Aggregator<?, ?> type) {
|
||||
set(type, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
Log.i(TAG, "Removing all aggregators");
|
||||
this.heartRate = null;
|
||||
this.cyclingCadence = null;
|
||||
this.cyclingDistanceSpeed = null;
|
||||
this.cyclingPower = null;
|
||||
this.runningDistanceSpeedCadence = null;
|
||||
this.barometer = null;
|
||||
}
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
@@ -116,7 +155,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
|
||||
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall());
|
||||
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall());
|
||||
}
|
||||
|
||||
if (cyclingPower != null && cyclingPower.hasValue()) {
|
||||
@@ -126,81 +165,49 @@ public final class SensorDataSet {
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
|
||||
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().distance());
|
||||
}
|
||||
|
||||
if (barometer != null && barometer.hasValue()) {
|
||||
trackPoint.setAltitudeGain(barometer.getValue().gain_m());
|
||||
trackPoint.setAltitudeLoss(barometer.getValue().loss_m());
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Log.i(TAG, "Resetting data");
|
||||
|
||||
if (heartRate != null) heartRate.reset();
|
||||
if (cyclingCadence != null) cyclingCadence.reset();
|
||||
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
|
||||
if (cyclingPower != null) cyclingPower.reset();
|
||||
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
|
||||
if (barometer != null) barometer.reset();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return (heartRate != null ? "" + heartRate : "")
|
||||
+ (cyclingCadence != null ? " " + cyclingCadence : "")
|
||||
+ (cyclingDistanceSpeed != null ? " " + cyclingDistanceSpeed : "")
|
||||
+ (cyclingPower != null ? " " + cyclingPower : "")
|
||||
+ (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : "");
|
||||
}
|
||||
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
|
||||
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName());
|
||||
|
||||
private void set(@NonNull SensorData<?> type, @Nullable SensorData<?> sensorData) {
|
||||
if (type instanceof SensorDataHeartRate) {
|
||||
this.heartRate = (SensorDataHeartRate) sensorData;
|
||||
if (type instanceof AggregatorHeartRate) {
|
||||
heartRate = (AggregatorHeartRate) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCyclingCadence) {
|
||||
SensorDataCyclingCadence previous = getCyclingCadence();
|
||||
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
|
||||
|
||||
if (sensorData != null && sensorData.equals(previous)) {
|
||||
Log.d(TAG, "onChanged: cadence data repeated.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.cyclingCadence = (SensorDataCyclingCadence) sensorData;
|
||||
if (this.cyclingCadence != null) {
|
||||
this.cyclingCadence.compute(previous);
|
||||
}
|
||||
if (type instanceof AggregatorCyclingCadence) {
|
||||
cyclingCadence = (AggregatorCyclingCadence) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCyclingDistanceSpeed) {
|
||||
SensorDataCyclingDistanceSpeed previous = getCyclingDistanceSpeed();
|
||||
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
|
||||
if (sensorData != null && sensorData.equals(previous)) {
|
||||
Log.d(TAG, "onChanged: cycling speed data repeated.");
|
||||
return;
|
||||
}
|
||||
Distance preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(); //TODO Fetch once and then listen for changes.
|
||||
|
||||
this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) sensorData;
|
||||
if (this.cyclingDistanceSpeed != null) {
|
||||
this.cyclingDistanceSpeed.compute(previous, preferenceWheelCircumference);
|
||||
}
|
||||
if (type instanceof AggregatorCyclingDistanceSpeed) {
|
||||
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCyclingPower) {
|
||||
this.cyclingPower = (SensorDataCyclingPower) sensorData;
|
||||
if (type instanceof AggregatorCyclingPower) {
|
||||
cyclingPower = (AggregatorCyclingPower) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataRunning) {
|
||||
SensorDataRunning previous = getRunningDistanceSpeedCadence();
|
||||
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
|
||||
if (sensorData != null && sensorData.equals(previous)) {
|
||||
Log.d(TAG, "onChanged: running speed data repeated.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.runningDistanceSpeedCadence = (SensorDataRunning) sensorData;
|
||||
if (this.runningDistanceSpeedCadence != null) {
|
||||
this.runningDistanceSpeedCadence.compute(previous);
|
||||
}
|
||||
if (type instanceof AggregatorRunning) {
|
||||
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorBarometer) {
|
||||
barometer = (AggregatorBarometer) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public interface SensorHandlerInterface {
|
||||
|
||||
List<ServiceMeasurementUUID> getServices();
|
||||
|
||||
Aggregator<?, ?> createEmptySensorData(String address);
|
||||
|
||||
void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
|
||||
@NonNull
|
||||
private Clock clock = new MonotonicClock();
|
||||
private SensorManager sensorManager;
|
||||
private final SensorManager sensorManager;
|
||||
|
||||
public TrackPointCreator(Callback service) {
|
||||
this.service = service;
|
||||
@@ -64,7 +64,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
return sensorManager.fill(trackPoint);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
public void stop() {
|
||||
sensorManager.stop(context);
|
||||
this.context = null;
|
||||
}
|
||||
@@ -139,11 +139,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
return sensorManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setSensorManager(SensorManager sensorManager) {
|
||||
this.sensorManager = sensorManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setClock(@NonNull String time) {
|
||||
this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET"));
|
||||
|
||||
@@ -49,8 +49,8 @@ public class ResetDialogPreference extends DialogPreference {
|
||||
Toast.makeText(activity, R.string.settings_layout_reset_done, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
if (activity instanceof ResetCallback) {
|
||||
((ResetCallback) activity).onReset();
|
||||
if (activity instanceof ResetCallback resetCallback) {
|
||||
resetCallback.onReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ public class SensorsSettingsFragment extends PreferenceFragmentCompat {
|
||||
private void setWheelCircumferenceInputFilter() {
|
||||
EditTextPreference wheelPreference = findPreference(getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key));
|
||||
wheelPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (newValue instanceof String) {
|
||||
if (newValue instanceof String newValueString) {
|
||||
try {
|
||||
int newValueInt = Integer.parseInt((String) newValue);
|
||||
int newValueInt = Integer.parseInt(newValueString);
|
||||
return newValueInt >= 100 && newValueInt < 4000;
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Entered string is no number.");
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
|
||||
public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPrefer
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_CADENCE);
|
||||
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingCadence.CYCLING_CADENCE);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
|
||||
|
||||
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_POWER);
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerCyclingPower.CYCLING_POWER);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
|
||||
public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE);
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerHeartRate;
|
||||
|
||||
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
@@ -27,6 +27,6 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES);
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerHeartRate.HEART_RATE_SUPPORTING_DEVICES);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
|
||||
public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
@@ -27,6 +27,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.RUNNING_SPEED_CADENCE);
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,12 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
|
||||
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
|
||||
Marker.Id markerId = null;
|
||||
if (bundleMarkerId instanceof Marker.Id) {
|
||||
markerId = (Marker.Id) bundleMarkerId;
|
||||
if (bundleMarkerId instanceof Marker.Id cast) {
|
||||
markerId = cast;
|
||||
}
|
||||
if (bundleMarkerId instanceof Long) {
|
||||
if (bundleMarkerId instanceof Long cast) {
|
||||
//Incoming Intent via Dashboard API.
|
||||
markerId = new Marker.Id((Long) bundleMarkerId);
|
||||
markerId = new Marker.Id(cast);
|
||||
}
|
||||
if (markerId == null) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
|
||||
@@ -75,7 +75,6 @@ public class ListItemUtils {
|
||||
// Match list_item_time_distance in list_item.xml
|
||||
|
||||
String time = StringUtils.formatElapsedTime(totalTime);
|
||||
// TODO unit system
|
||||
String distance = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context)
|
||||
|
||||
@@ -20,10 +20,10 @@ public class ViewUtils {
|
||||
|
||||
for (int i = 0; i < view.getChildCount(); i++) {
|
||||
final View child = view.getChildAt(i);
|
||||
if (child instanceof ViewGroup) {
|
||||
makeClickableLinks((ViewGroup) child);
|
||||
} else if (child instanceof TextView) {
|
||||
((TextView) child).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
if (child instanceof ViewGroup childVg) {
|
||||
makeClickableLinks(childVg);
|
||||
} else if (child instanceof TextView childTv) {
|
||||
childTv.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public class ExportUtils {
|
||||
}
|
||||
|
||||
TrackExporter trackExporter = exportTask.getTrackFileFormat().createTrackExporter(context, contentProviderUtils);
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri, "wt")) {
|
||||
if (!trackExporter.writeTrack(tracks, outputStream)) {
|
||||
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
|
||||
throw new RuntimeException("Unable to delete exportDocumentFile");
|
||||
|
||||
@@ -240,9 +240,9 @@ public class StringUtils {
|
||||
public static OffsetDateTime parseTime(String xmlDateTime) {
|
||||
try {
|
||||
TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parseBest(xmlDateTime, ZonedDateTime::from, LocalDateTime::from);
|
||||
if (t instanceof LocalDateTime) {
|
||||
if (t instanceof LocalDateTime localDateTime) {
|
||||
Log.w(TAG, "Date does not contain timezone information: using UTC.");
|
||||
t = ((LocalDateTime) t).atZone(ZoneOffset.UTC);
|
||||
t = localDateTime.atZone(ZoneOffset.UTC);
|
||||
}
|
||||
return OffsetDateTime.from(t);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -593,4 +593,5 @@ limitations under the License.
|
||||
<string name="settings_ui_dynamic_colors_title">Dynamické barvy</string>
|
||||
<string name="settings_recording_title">Záznam</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Používat dynamické barvy definované uživatelem (pouze Android 12+; vyžaduje ruční restart)</string>
|
||||
<string name="permission_recording_failed">Aplikace OpenTrack vyžaduje oprávnění k přístupu k vaší poloze a zařízením poblíž.</string>
|
||||
</resources>
|
||||
@@ -645,4 +645,8 @@ limitations under the License.
|
||||
<string name="settings_announcements_idle_title">Inactivo</string>
|
||||
<string name="settings_recording_idle_timeout_title">Umbral de inactividad</string>
|
||||
<string name="value_int_seconds">%1$d s (recomendado)</string>
|
||||
<string name="settings_ui_dynamic_colors_title">Colores dinámicos</string>
|
||||
<string name="settings_recording_title">Grabando</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Utilizar colores dinámicos definidos por el usuario (sólo Android 12+; requiere un reinicio manual)</string>
|
||||
<string name="permission_recording_failed">OpenTracks requiere permiso para acceder a su ubicación y a los dispositivos cercanos.</string>
|
||||
</resources>
|
||||
@@ -595,4 +595,5 @@ Se o GPS informa sobre datos pouco precisos (ex.: localización, velocidade, ele
|
||||
<string name="settings_ui_dynamic_colors_title">Cores Dinámicas</string>
|
||||
<string name="settings_recording_title">Ao gravar</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Usar Cores Dinámicas definidas pola usuaria (Só Android 12+; require reiniciar)</string>
|
||||
<string name="permission_recording_failed">OpenTracks precisa permiso para acceder á túa localización así como a dispositivos próximos.</string>
|
||||
</resources>
|
||||
@@ -589,4 +589,5 @@ limitations under the License.
|
||||
<string name="export_progress_summary_new_msg">Nove datoteke su izvezene</string>
|
||||
<string name="settings_recording_idle_timeout_title">Prag neaktivnosti</string>
|
||||
<string name="export_cannot_write_to_dir">Nije moguće pisati u direktorij izvoza.</string>
|
||||
<string name="permission_recording_failed">OpenTracks zahtijeva dozvolu za pristup tvojoj lokaciji kao i uređajima u blizini.</string>
|
||||
</resources>
|
||||
@@ -65,7 +65,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">vliegtuig</string>
|
||||
<string name="activity_type_atv">quad</string>
|
||||
<string name="activity_type_biking">fietsen</string>
|
||||
<string name="activity_type_biking">motorfietsen</string>
|
||||
<string name="activity_type_blimp">luchtballon</string>
|
||||
<string name="activity_type_boat">boot</string>
|
||||
<string name="activity_type_climbing">klimmen</string>
|
||||
@@ -200,7 +200,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<!-- Marker List -->
|
||||
<string name="marker_list_empty_message">Er zijn geen markeringen</string>
|
||||
<!-- Menu -->
|
||||
<string name="menu_aggregated_statistics">Statistieken</string>
|
||||
<string name="menu_aggregated_statistics">Statistiek</string>
|
||||
<string name="menu_delete">Verwijderen</string>
|
||||
<string name="menu_edit">Bewerken</string>
|
||||
<string name="menu_export_all">Exporteer alle (een track per bestand</string>
|
||||
@@ -223,7 +223,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<!-- Search -->
|
||||
<!-- Sensor State -->
|
||||
<string name="sensor_state_heart_rate">Hartslag</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d spm</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d /min</string>
|
||||
<string name="sensor_state_power">Vermogen</string>
|
||||
<string name="sensor_could_not_scan">Kon niet scannen naar Bluetooth-apparaten (fout %1$i).</string>
|
||||
<string name="bluetooth_disabled">Schakel Bluetooth in.</string>
|
||||
@@ -237,7 +237,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Toon tijdens de opname de statistieken zonder het apparaat te ontgrendelen.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Houd het scherm aan</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Houd tijdens de opname het scherm aan.</string>
|
||||
<string name="settings_recording_default_activity_title">Standaardtype activiteit</string>
|
||||
<string name="settings_recording_default_activity_title">Activiteittype</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s tussen opgenomen locaties</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Er wordt een nieuw segment gemaakt als de afstand tussen twee locaties groter is dan %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Maximale afstand</string>
|
||||
@@ -247,17 +247,17 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Datum (ISO 8601)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Datum (lokaal)</string>
|
||||
<string name="settings_recording_track_name_number_option">Nummer</string>
|
||||
<string name="settings_recording_track_name_title">Standaardroutenaam</string>
|
||||
<string name="settings_recording_track_name_title">Routenaam</string>
|
||||
<!-- Settings Reset -->
|
||||
<string name="settings_reset_confirm_message">Alle instellingen worden teruggezet op de standaardwaarden.</string>
|
||||
<string name="settings_reset_confirm_message">Alle instellingen worden hersteld naar standaardwaarden.</string>
|
||||
<string name="settings_reset_confirm_title">Nu opnieuw instellen?</string>
|
||||
<string name="settings_reset_done">Voor alle instellingen zijn de standaardwaarden teruggezet</string>
|
||||
<string name="settings_reset_done">Alle instellingen zijn naar standaardwaarden teruggezet</string>
|
||||
<!-- Settings Sensor -->
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_rate_title">Voorkeursverhouding</string>
|
||||
<string name="settings_stats_units_imperial">Engels (mi, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrisch (km, m)</string>
|
||||
<string name="settings_stats_units_title">Voorkeurseenheden</string>
|
||||
<string name="settings_stats_units_title">Lengtemaat</string>
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file_body">Misschien ben je geïnteresseerd in deze route.
|
||||
\n
|
||||
@@ -469,12 +469,11 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="stats_sensors_cadence">Ritme</string>
|
||||
<string name="stats_sensors_heart_rate">Hartslag</string>
|
||||
<string name="settings_layout_reset">Aangepaste lay-out terugzetten</string>
|
||||
<string name="settings_layout_reset_confirm_message">Alle lay-out instellingen worden teruggezet naar de standaardwaarden. Tracks worden niet gewist.</string>
|
||||
<string name="settings_layout_reset_confirm_message">Alle lay-out instellingen worden hrrsteld naar de standaardwaarden. Routes worden niet gewist.</string>
|
||||
<string name="settings_public_api_enabled_summary_on">Andere geïnstalleerde apps kunnen opnames starten of stoppen.</string>
|
||||
<string name="stats_sensors_power">Vermogen</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">Opgenomen gegevens worden niet automatisch met andere apps gedeeld.</string>
|
||||
<string name="voice_per_kilometer">per kilometer</string>
|
||||
<string name="voice_per_mile">per mijl</string>
|
||||
<string name="voice_per_mile">/mijl</string>
|
||||
<string name="lap_time">Rondetijd</string>
|
||||
<string name="pace">Tempo</string>
|
||||
<string name="lap_speed">Ronde snelheid</string>
|
||||
@@ -503,7 +502,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="no_compatible_file_manager_installed">Geen geschikte bestandsbeheerder geïnstalleerd</string>
|
||||
<string name="generic_undo">Terugzetten</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_on">Alleen Bluetooth-apparaten tonen die gevraagde diensten melden</string>
|
||||
<string name="settings_activity_defaults_category">Standaardinstellingen van een activiteit</string>
|
||||
<string name="settings_activity_defaults_category">Standaardinstellingen activiteit</string>
|
||||
<string name="settings_import_export_summary">Invoer, uitvoer, automatische uitvoer</string>
|
||||
<string name="settings_units_category">Eenheden</string>
|
||||
<string name="settings_reset_summary">Instellingen terugzetten op standaardwaarden</string>
|
||||
@@ -514,14 +513,14 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="settings_open_tracks_summary">Alles over het OpenTracks Project</string>
|
||||
<string name="activity_type_workout">training</string>
|
||||
<string name="settings_layout_reset_confirm_title">Lay-outs terugzetten\?</string>
|
||||
<string name="settings_layout_reset_done">Alle lay-out instellingen zijn teruggezet naar de standaardwaarden</string>
|
||||
<string name="settings_layout_reset_done">Alle lay-out instellingen zijn hersteld naar standaardwaarden</string>
|
||||
<string name="field_set_primary">Primair</string>
|
||||
<string name="gps_recording_status">%1$s: %2$s</string>
|
||||
<string name="gps_recording_without_signal">OpenTracks neemt op zonder GPS gegevens.</string>
|
||||
<string name="generic_dismiss">Afwijzen</string>
|
||||
<string name="menu_select_layout">Lay-out kiezen</string>
|
||||
<string name="settings_defaults_title">Standaard Eenheden en Activiteit</string>
|
||||
<string name="settings_defaults_summary">Voorkeurseenheden, standaardactiviteit, standaardtracknaam</string>
|
||||
<string name="settings_defaults_summary">Voorkeurslengtemaat, standaardactiviteit, standaardroutenaam</string>
|
||||
<string name="settings_gps_summary">Tijdsinterval, afstandsinterval, maximale afstand, nauwkeurigheid</string>
|
||||
<string name="settings_sensors_title">Bluetooth sensoren</string>
|
||||
<string name="settings_sensors_summary">Fiets sensoren, wielomtrek, loop sensoren</string>
|
||||
@@ -532,7 +531,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="custom_layout_list_layout_removed">Lay-out verwijderd</string>
|
||||
<string name="custom_layout_select_layout">Lay-out kiezen</string>
|
||||
<string name="stats_custom_layout_default_layout">Standaard lay-out</string>
|
||||
<string name="settings_all_reset">Teruggezet</string>
|
||||
<string name="settings_all_reset">Herstellen</string>
|
||||
<string name="settings_export_filename_title">Format van de bestandsnaam</string>
|
||||
<string name="error_crash_title">%1$s gecrasht</string>
|
||||
<string name="report_bug">Bug rapporteren</string>
|
||||
@@ -552,7 +551,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="introduction_osm_dashboard">OpenTracks zelf biedt geen kaart. Installeer OSMDashboard om uw opnames op een kaart te bekijken.</string>
|
||||
<string name="unit_nautical_mile">NM</string>
|
||||
<string name="value_integer_knots">%1$d knopen</string>
|
||||
<string name="voice_per_nautical_mile">per zeemijl</string>
|
||||
<string name="voice_per_nautical_mile">/NM</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d routes</string>
|
||||
<string name="voice_on_device_speaker_title">Gebruik toestelspeaker</string>
|
||||
<string name="settings_public_api_package_title">Pakketnaam voor openbare API</string>
|
||||
@@ -575,8 +574,6 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 nautisch mijl} other {{n,number,#.0} nautische mijlen} }</string>
|
||||
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {1 kilometer} other {{n,number,#.0} kilometers} }</string>
|
||||
<string name="sensor_state_power_unit">W</string>
|
||||
<string name="sensor_state_heart_rate_unit">bpm</string>
|
||||
<string name="sensor_state_cadence_unit">rpm</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">Alle ontdekbare Bluetooth-apparaten weergeven</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 knoop} other {{n,number,#.0} knopen} }</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 mijl per uur} other {{n,number,#.0} mijlen per uur} }</string>
|
||||
@@ -594,4 +591,8 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="settings_ui_dynamic_colors_title">Dynamische kleuren</string>
|
||||
<string name="settings_recording_title">Opname</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Gebruik door de gebruiker gedefinieerde dynamische kleuren (alleen Android 12+; vereist een handmatige herstart)</string>
|
||||
<string name="permission_recording_failed">OpenTracks heeft toestemming nodig om toegang te krijgen tot zowel uw locatie als nabijgelegen apparaten.</string>
|
||||
<string name="sensor_state_heart_rate_unit">/min</string>
|
||||
<string name="sensor_state_cadence_unit">/min</string>
|
||||
<string name="voice_per_kilometer">/km</string>
|
||||
</resources>
|
||||
@@ -260,16 +260,16 @@
|
||||
<string name="waypoint_type_water_fountain">fonte de água</string>
|
||||
<string name="waypoint_type_waypoint">ponto de referência</string>
|
||||
<string name="about_preference_title">Sobre o OpenTracks</string>
|
||||
<string name="about_description"><i>OpenTracks</i> é uma aplicação de rastreamento de desportes que respeita totalmente a sua privacidade.</string>
|
||||
<string name="about_description"><i>OpenTracks</i> é uma aplicação de rastreamento de desportos que respeita integralmente a sua privacidade.</string>
|
||||
<string name="about_url">Página do projeto: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_third_party_data">Dados de terceiros</string>
|
||||
<string name="help_track_map_title">Como mostrar uma pista num mapa\?</string>
|
||||
<string name="help_track_map_title">Como mostrar uma trilho num mapa?</string>
|
||||
<string name="help_storage_pictures_title">Como o OpenTracks armazena fotos tiradas como marcadores\?</string>
|
||||
<string name="help_storage_pictures_content">Eles são armazenados no armazenamento interno da app (ou seja, acessíveis apenas no OpenTracks).</string>
|
||||
<string name="help_storage_pictures_content">Estes são armazenados no armazenamento interno da app (ou seja, acessíveis apenas pelo OpenTracks).</string>
|
||||
<string name="help_data_google_play_services_title">O OpenTracks requer/usa o Google Play Services\?</string>
|
||||
<string name="help_data_google_play_services_content">Não</string>
|
||||
<string name="help_recording_title">Como o OpenTracks regista dados\?</string>
|
||||
<string name="help_support_title">Como posso oferecer suporte a OpenTracks\?</string>
|
||||
<string name="help_recording_title">Como guarda o OpenTracks dados?</string>
|
||||
<string name="help_support_title">Como posso suportar o OpenTracks?</string>
|
||||
<string name="open_track_as_trackfileformat">Abrir como %1$s</string>
|
||||
<string name="activity_type_escooter">escolta</string>
|
||||
<string name="activity_type_swimming_open">nadando em águas abertas</string>
|
||||
@@ -311,32 +311,32 @@
|
||||
<string name="field_remove_from_layout">Remover</string>
|
||||
<string name="about_libraries">Bibliotecas</string>
|
||||
<string name="help_feature_title">OpenTracks tem os seguintes recursos:</string>
|
||||
<string name="help_feature_content">* criar pistas GPS
|
||||
<string name="help_feature_content">* criar trilhos GPS
|
||||
\n * utilizar um sensor do ritmo cardíaco (apenas Bluetooth LE)
|
||||
\n * anúncios de voz (por tempo ou distância)
|
||||
\n * criar marcadores (incl. imagens)
|
||||
\n * funciona completamente offline
|
||||
\n * mostrar pistas num mapa (ver abaixo)</string>
|
||||
\n * mostrar trilhos num mapa (ver abaixo)</string>
|
||||
<string name="about_version_name">Nome da versão: %1$s</string>
|
||||
<string name="about_version_code">Código da versão: %1$d</string>
|
||||
<string name="about_history">OpenTracks é um fork do <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (descontinuado em 2016).</string>
|
||||
<string name="help_track_export_title">Como posso exportar/compartilhar trilhas gravadas\?</string>
|
||||
<string name="help_track_export_content"> As trilhas podem ser exportadas das configurações como KMZ, KML ou GPX. KMZ/KML todos os dados; Localizações apenas GPX. As trilhas compartilhadas por meio do <i>ícone de compartilhamento</i> são compartilhadas como KMZ. </string>
|
||||
<string name="help_track_export_title">Como posso exportar/compartilhar trilhos gravados?</string>
|
||||
<string name="help_track_export_content"> Os trilhos podem ser exportados como configurações nos formatos KMZ, KML ou GPX. KMZ/KML exporta todos os dados; GPX exporta apenas localizações. Os trilhos partilhados por meio do <i>ícone de partilha</i> são partilhados como KMZ. </string>
|
||||
<string name="help_track_map_content"> Utilize o pequeno ícone do mapa.
|
||||
\n a) (actualizações ao vivo) Instalar <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Fornece mapas online ou offline através de <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
\n Na verdade, o OSMDashboard é uma aplicação separada que só pode aceder (temporariamente) aos dados da(s) faixa(s) que vão ser mostrados.
|
||||
\n Na verdade, o OSMDashboard é uma aplicação separada que só pode aceder (temporariamente) aos dados do(s) trilho(s) que vão ser mostrados.
|
||||
\n b) Através de uma aplicação de mapa normal (por exemplo, <a href="http://osmand.net/">OsmAnd</a> e <a href="https://maps.me/">MAPS.ME</a>). Estas aplicações precisam de suportar o formato de ficheiro KMZ (apenas locais e timestamps). De facto, uma aplicação precisa de suportar <i>ACTION_VIEW</i> com MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||
<string name="help_recording_content">OpenTracks usa o sensor GPS do aparelho para registar a localização continuamente. Se o aparelho GPS relatar dados imprecisos (por exemplo, localização, velocidade, elevação), o OpenTracks não pode fazer nada a respeito.</string>
|
||||
<string name="help_data_storage_title">Como o OpenTracks armazena dados do sensor\?</string>
|
||||
<string name="help_data_storage_content"> Os dados do sensor são armazenados por local registado. Em termos de sensor de frequência cardíaca, isso significa que apenas <i>uma</i> medição de frequência cardíaca é armazenada por local. Se estiver parado ou dentro de casa, nenhum dado do sensor será registrdo (mas será mostrado). </string>
|
||||
<string name="help_recording_content">OpenTracks usa o sensor GPS do aparelho para registar continuamente a localização. Se o aparelho GPS apresentar dados imprecisos (por exemplo, localização, velocidade, elevação), o OpenTracks não pode fazer nada a esse respeito.</string>
|
||||
<string name="help_data_storage_title">Como armazena o OpenTracks os dados dos sensores?</string>
|
||||
<string name="help_data_storage_content"> Os dados do sensor são armazenados por local registado. Relativamente ao sensor de frequência cardíaca, isso significa que apenas <i>uma</i> medição de frequência cardíaca é armazenada por local. Se estiver parado ou dentro de casa, nenhum dado do sensor será registado (mas será mostrado). </string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks não está gravando locais\?</string>
|
||||
<string name="help_gps_troubleshooting_content"> O que se pode fazer:
|
||||
\n a) verificar se o GPS está activado,
|
||||
<string name="help_gps_troubleshooting_content"> O que pode fazer:
|
||||
\n a) verificar se o GPS está activo,
|
||||
\n b) melhorar a recepção GPS (por exemplo, ir para o exterior),
|
||||
\n c) verificar com <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, e
|
||||
\n d) desactivar a poupança de energia.
|
||||
\n
|
||||
\n Se tudo isto não ajudar, por favor, informe um erro. </string>
|
||||
\n Se tudo isto não ajudar, por favor, abra um bug. </string>
|
||||
<string name="help_support_content"> Pode:
|
||||
\n a) espalhar a palavra sobre OpenTracks,
|
||||
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">propose features</a>,
|
||||
|
||||
+301
-260
@@ -15,87 +15,87 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
--><resources>
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">Авиаспорт</string>
|
||||
<string name="activity_type_atv">Езда на внедорожниках</string>
|
||||
<string name="activity_type_biking">велосипед</string>
|
||||
<string name="activity_type_blimp">Дирижабли</string>
|
||||
<string name="activity_type_airplane">самолет</string>
|
||||
<string name="activity_type_atv">квадроцикл</string>
|
||||
<string name="activity_type_biking">байк</string>
|
||||
<string name="activity_type_blimp">дирижабль</string>
|
||||
<string name="activity_type_boat">лодка</string>
|
||||
<string name="activity_type_climbing">Альпинизм и скалолазание</string>
|
||||
<string name="activity_type_commercial_airplane">Пассажирские самолеты</string>
|
||||
<string name="activity_type_cross_country_skiing">лыжные гонки</string>
|
||||
<string name="activity_type_cycling">Езда на велосипеде</string>
|
||||
<string name="activity_type_dirt_bike">Дертбайк</string>
|
||||
<string name="activity_type_donkey_back_riding">Верховая езда на ослах</string>
|
||||
<string name="activity_type_driving">на автомобиле</string>
|
||||
<string name="activity_type_driving_bus">Вождение автобуса</string>
|
||||
<string name="activity_type_driving_car">Вождение автомобиля</string>
|
||||
<string name="activity_type_ferry">Паром</string>
|
||||
<string name="activity_type_frisbee">Фрисби</string>
|
||||
<string name="activity_type_gliding">Планеризм</string>
|
||||
<string name="activity_type_hang_gliding">Дельтапланеризм</string>
|
||||
<string name="activity_type_helicopter">Вертолеты</string>
|
||||
<string name="activity_type_hiking">Пеший туризм</string>
|
||||
<string name="activity_type_horse_back_riding">Верховая езда на лошадях</string>
|
||||
<string name="activity_type_hot_air_balloon">Воздухоплавание</string>
|
||||
<string name="activity_type_ice_sailing">Зимний парусный спорт</string>
|
||||
<string name="activity_type_inline_skating">Катание на роликовых коньках</string>
|
||||
<string name="activity_type_kayaking">Каякинг</string>
|
||||
<string name="activity_type_kite_surfing">Кайтсерфинг</string>
|
||||
<string name="activity_type_land_sailing">Сухопутный парусный спорт</string>
|
||||
<string name="activity_type_mixed_type">Смешанный</string>
|
||||
<string name="activity_type_motor_bike">Езда на мотоцикле</string>
|
||||
<string name="activity_type_motor_boating">Водно-моторный спорт</string>
|
||||
<string name="activity_type_mountain_biking">Горный велосипед</string>
|
||||
<string name="activity_type_off_trail_hiking">Ходьба по бездорожью</string>
|
||||
<string name="activity_type_other">Другое</string>
|
||||
<string name="activity_type_paddling">Гребля</string>
|
||||
<string name="activity_type_para_gliding">Парапланеризм</string>
|
||||
<string name="activity_type_rc_airplane">Радиоуправляемые самолеты</string>
|
||||
<string name="activity_type_rc_boat">Радиоуправляемые лодки</string>
|
||||
<string name="activity_type_rc_helicopter">Радиоуправляемые вертолеты</string>
|
||||
<string name="activity_type_riding">Верховая езда</string>
|
||||
<string name="activity_type_road_biking">Шоссейный велосипед</string>
|
||||
<string name="activity_type_climbing">скалолазание</string>
|
||||
<string name="activity_type_commercial_airplane">коммерческий авиарейс</string>
|
||||
<string name="activity_type_cross_country_skiing">беговые лыжи</string>
|
||||
<string name="activity_type_cycling">велосипед</string>
|
||||
<string name="activity_type_dirt_bike">кроссовый мотоцикл</string>
|
||||
<string name="activity_type_donkey_back_riding">верховая езда на осле</string>
|
||||
<string name="activity_type_driving">управление транспортным средством</string>
|
||||
<string name="activity_type_driving_bus">управление автобусом</string>
|
||||
<string name="activity_type_driving_car">управление автомобилем</string>
|
||||
<string name="activity_type_ferry">паром</string>
|
||||
<string name="activity_type_frisbee">фрисби</string>
|
||||
<string name="activity_type_gliding">планер</string>
|
||||
<string name="activity_type_hang_gliding">дельтаплан</string>
|
||||
<string name="activity_type_helicopter">вертолет</string>
|
||||
<string name="activity_type_hiking">поход</string>
|
||||
<string name="activity_type_horse_back_riding">верховая езда на лошади</string>
|
||||
<string name="activity_type_hot_air_balloon">воздушный шар</string>
|
||||
<string name="activity_type_ice_sailing">ледовый яхтинг</string>
|
||||
<string name="activity_type_inline_skating">роликовые коньки</string>
|
||||
<string name="activity_type_kayaking">каяк</string>
|
||||
<string name="activity_type_kite_surfing">кайтсерф</string>
|
||||
<string name="activity_type_land_sailing">сухопутный яхтинг</string>
|
||||
<string name="activity_type_mixed_type">смешанная деятельность</string>
|
||||
<string name="activity_type_motor_bike">мотоцикл</string>
|
||||
<string name="activity_type_motor_boating">моторная лодка</string>
|
||||
<string name="activity_type_mountain_biking">горный велосипед</string>
|
||||
<string name="activity_type_off_trail_hiking">поход вне троп</string>
|
||||
<string name="activity_type_other">другое</string>
|
||||
<string name="activity_type_paddling">гребля</string>
|
||||
<string name="activity_type_para_gliding">параплан</string>
|
||||
<string name="activity_type_rc_airplane">радиоуправляемый самолет</string>
|
||||
<string name="activity_type_rc_boat">радиоуправляемая лодка</string>
|
||||
<string name="activity_type_rc_helicopter">радиоуправляемый вертолет</string>
|
||||
<string name="activity_type_riding">верховая езда</string>
|
||||
<string name="activity_type_road_biking">шоссейный велосипед</string>
|
||||
<string name="activity_type_roller_skiing">роликовые лыжи</string>
|
||||
<string name="activity_type_rowing">Гребля академическая</string>
|
||||
<string name="activity_type_running">Бег</string>
|
||||
<string name="activity_type_sailing">Парусный спорт</string>
|
||||
<string name="activity_type_seaplane">Гидросамолеты</string>
|
||||
<string name="activity_type_skate_boarding">Скейтбординг</string>
|
||||
<string name="activity_type_skating">Скейтинг</string>
|
||||
<string name="activity_type_skiing">Лыжный спорт</string>
|
||||
<string name="activity_type_sky_jumping">Прыжки с парашютом</string>
|
||||
<string name="activity_type_sled">Катание на санях</string>
|
||||
<string name="activity_type_snow_boarding">Сноубординг</string>
|
||||
<string name="activity_type_snow_shoeing">Ходьба в снегоступах</string>
|
||||
<string name="activity_type_speed_walking">Спортивная ходьба</string>
|
||||
<string name="activity_type_street_running">Бег по шоссе</string>
|
||||
<string name="activity_type_surfing">Серфинг</string>
|
||||
<string name="activity_type_track_cycling">Езда по велотреку</string>
|
||||
<string name="activity_type_track_running">Легкоатлетический бег</string>
|
||||
<string name="activity_type_trail_hiking">Ходьба по пересеченной местности</string>
|
||||
<string name="activity_type_trail_running">Бег по пересеченной местности</string>
|
||||
<string name="activity_type_train">Поезда</string>
|
||||
<string name="activity_type_ultimate_frisbee">Алтимат</string>
|
||||
<string name="activity_type_unknown">Неизвестно</string>
|
||||
<string name="activity_type_wakeboarding">Вейкбординг</string>
|
||||
<string name="activity_type_walking">Ходьба</string>
|
||||
<string name="activity_type_water_skiing">Катание на водных лыжах</string>
|
||||
<string name="activity_type_wind_surfing">Виндсерфинг</string>
|
||||
<string name="activity_type_rowing">гребля академическая</string>
|
||||
<string name="activity_type_running">бег</string>
|
||||
<string name="activity_type_sailing">парусная лодка</string>
|
||||
<string name="activity_type_seaplane">гидросамолет</string>
|
||||
<string name="activity_type_skate_boarding">скейтборд</string>
|
||||
<string name="activity_type_skating">скейт</string>
|
||||
<string name="activity_type_skiing">лыжи</string>
|
||||
<string name="activity_type_sky_jumping">прыжок с парашютом</string>
|
||||
<string name="activity_type_sled">сани</string>
|
||||
<string name="activity_type_snow_boarding">сноуборд</string>
|
||||
<string name="activity_type_snow_shoeing">снегоступы</string>
|
||||
<string name="activity_type_speed_walking">быстрая ходьба</string>
|
||||
<string name="activity_type_street_running">бег уличный</string>
|
||||
<string name="activity_type_surfing">серф</string>
|
||||
<string name="activity_type_track_cycling">велотрек</string>
|
||||
<string name="activity_type_track_running">бег легкоатлетический</string>
|
||||
<string name="activity_type_trail_hiking">поход по пересеченной местности</string>
|
||||
<string name="activity_type_trail_running">бег по пересеченной местности</string>
|
||||
<string name="activity_type_train">поезд</string>
|
||||
<string name="activity_type_ultimate_frisbee">алтимат</string>
|
||||
<string name="activity_type_unknown">неизвестно</string>
|
||||
<string name="activity_type_wakeboarding">вейкборд</string>
|
||||
<string name="activity_type_walking">ходьба</string>
|
||||
<string name="activity_type_water_skiing">водные лыжи</string>
|
||||
<string name="activity_type_wind_surfing">виндсерф</string>
|
||||
<!-- Description -->
|
||||
<string name="description_activity_type">Вид активности: %1$s</string>
|
||||
<string name="description_average_moving_pace_in_minute">Средний темп при движении: %1$s мин/км (%2$s мин/мил)</string>
|
||||
<string name="description_average_moving_speed">Средняя скорость при движении: %1$.2f км/ч (%2$.1f мил/ч)</string>
|
||||
<string name="description_average_pace_in_minute">Средний темп: %1$s мин/км (%2$s мин/мил)</string>
|
||||
<string name="description_average_speed">Средняя скорость: %1$.2f км/ч (%2$.1f мил/ч)</string>
|
||||
<string name="description_altitude_gain">Набор высоты: %1$d м (%2$d фт)</string>
|
||||
<string name="description_average_moving_pace_in_minute">Средний темп в движении: %1$s мин/км (%2$s минут на милю)</string>
|
||||
<string name="description_average_moving_speed">Средняя скорость в движении: %1$.2f км/ч (%2$.1f миль/ч)</string>
|
||||
<string name="description_average_pace_in_minute">Средний темп: %1$s мин/км (%2$s минут на милю)</string>
|
||||
<string name="description_average_speed">Средняя скорость: %1$.2f км/ч (%2$.1f миль/ч)</string>
|
||||
<string name="description_altitude_gain">Набор высоты: %1$d м (%2$d футов)</string>
|
||||
<string name="description_altitude_imperial">Высота (футы)</string>
|
||||
<string name="description_altitude_metric">Высота (м)</string>
|
||||
<string name="description_fastest_pace_in_minute">Максимальный темп: %1$s мин/км (%2$s мин/мил)</string>
|
||||
<string name="description_max_altitude">Макс. высота: %1$d м (%2$d фт)</string>
|
||||
<string name="description_max_speed">Макс. скорость: %1$.2f км/ч (%2$.1f мил/ч)</string>
|
||||
<string name="description_min_altitude">Мин. высота: %1$d м (%2$d фт)</string>
|
||||
<string name="description_moving_time">Время движения: %1$s</string>
|
||||
<string name="description_pace_imperial">Темп (мин/мил)</string>
|
||||
<string name="description_fastest_pace_in_minute">Макс. темп: %1$s мин/км (%2$s минут на милю)</string>
|
||||
<string name="description_max_altitude">Макс. высота: %1$d м (%2$d футов)</string>
|
||||
<string name="description_max_speed">Макс. скорость: %1$.2f км/ч (%2$.1f миль/ч)</string>
|
||||
<string name="description_min_altitude">Мин. высота: %1$d м (%2$d футов)</string>
|
||||
<string name="description_moving_time">Время в движении: %1$s</string>
|
||||
<string name="description_pace_imperial">Темп (минут на милю)</string>
|
||||
<string name="description_pace_metric">Темп (мин/км)</string>
|
||||
<string name="description_recorded_time">Записано: %1$s</string>
|
||||
<string name="description_speed_ms">Скорость (м/с)</string>
|
||||
@@ -105,10 +105,10 @@ limitations under the License.
|
||||
<string name="description_speed_imperial">Скорость (миль/ч)</string>
|
||||
<string name="description_speed_metric">Скорость (км/ч)</string>
|
||||
<string name="description_time">Время</string>
|
||||
<string name="description_total_distance">Общее расстояние: %1$.2f км (%2$.1f мил.)</string>
|
||||
<string name="description_total_distance">Общее расстояние: %1$.2f км (%2$.1f миль)</string>
|
||||
<string name="description_total_time">Общее время: %1$s</string>
|
||||
<!-- Export -->
|
||||
<string name="export_progress_message">Экспорт в папку %1$s…</string>
|
||||
<string name="export_progress_message">Экспорт в %1$s…</string>
|
||||
<!-- External Storage -->
|
||||
<!-- Generic -->
|
||||
<string name="generic_add">Добавить</string>
|
||||
@@ -121,8 +121,8 @@ limitations under the License.
|
||||
<string name="generic_save">Сохранить</string>
|
||||
<string name="generic_tracks">Треки</string>
|
||||
<!-- Gps -->
|
||||
<string name="gps_starting">Включение GPS-модуля</string>
|
||||
<string name="gps_wait_for_better_signal">Поиск спутников GPS</string>
|
||||
<string name="gps_starting">Запуск GPS</string>
|
||||
<string name="gps_wait_for_better_signal">Ожидание улучшения приема GPS</string>
|
||||
<string name="gps_wait_for_signal">Ожидание сигнала GPS</string>
|
||||
<!-- Image -->
|
||||
<string name="image_arrow">Стрелка</string>
|
||||
@@ -132,14 +132,14 @@ limitations under the License.
|
||||
<string name="image_stop">Стоп</string>
|
||||
<string name="image_track">Трек</string>
|
||||
<!-- Import -->
|
||||
<string name="import_progress_message">Импорт из папки %1$s…</string>
|
||||
<string name="import_progress_message">Импорт из %1$s…</string>
|
||||
<!-- Marker -->
|
||||
<string name="marker_add_error">Невозможно добавить маркер. Нет сигнала GPS. Повторите попытку.</string>
|
||||
<string name="marker_add_success">Маркер добавлен</string>
|
||||
<string name="marker_add_canceled">Действие отменено, маркер не добавлен</string>
|
||||
<string name="marker_delete_one_confirm_message">Маркер будет удален без возможности восстановления</string>
|
||||
<string name="marker_add_canceled">Отменено, маркер не добавлен</string>
|
||||
<string name="marker_delete_one_confirm_message">Маркер будет удален с устройства без возможности восстановления</string>
|
||||
<string name="marker_delete_one_confirm_title">Удалить маркер?</string>
|
||||
<string name="marker_delete_multiple_confirm_message">Выбранные маркеры будут удалены без возможности восстановления</string>
|
||||
<string name="marker_delete_multiple_confirm_message">Выбранные маркеры будут удалены с устройства без возможности восстановления</string>
|
||||
<string name="marker_name_format">Маркер %1$d</string>
|
||||
<string name="marker_title">%1$d из %2$d</string>
|
||||
<!-- Marker Detail -->
|
||||
@@ -151,59 +151,59 @@ limitations under the License.
|
||||
<string name="menu_aggregated_statistics">Статистика</string>
|
||||
<string name="menu_delete">Удалить</string>
|
||||
<string name="menu_edit">Изменить</string>
|
||||
<string name="menu_export_all">Экспортировать все</string>
|
||||
<string name="menu_export_all">Экспортировать все треки в отдельные файлы</string>
|
||||
<string name="menu_help">Справка</string>
|
||||
<string name="menu_import_all">Импортировать все</string>
|
||||
<string name="menu_insert_marker">Вставить маркер</string>
|
||||
<string name="menu_insert_marker">Добавить маркер</string>
|
||||
<string name="menu_insert_photo">Добавить фото</string>
|
||||
<string name="menu_markers">Маркеры</string>
|
||||
<string name="menu_search">Поиск</string>
|
||||
<string name="menu_select_all">Выбрать все</string>
|
||||
<string name="menu_settings">Настройки</string>
|
||||
<string name="menu_share">Опубликовать</string>
|
||||
<string name="menu_share">Поделиться</string>
|
||||
<string name="menu_show_on_map">Показать на карте</string>
|
||||
<!-- OpenTracks -->
|
||||
<string name="recording_service">Служба записи треков</string>
|
||||
<string name="recording_service">Служба записи</string>
|
||||
<!-- Search -->
|
||||
<!-- Sensor State -->
|
||||
<string name="sensor_state_heart_rate">Пульс</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d уд/мин</string>
|
||||
<string name="sensor_state_power">Энергия</string>
|
||||
<string name="sensor_state_power">Мощность</string>
|
||||
<!-- Settings Advanced -->
|
||||
<string name="settings_advanced">Расширенные</string>
|
||||
<!-- Settings Chart -->
|
||||
<string name="settings_chart_by_distance">Расстояние</string>
|
||||
<string name="settings_chart_by_time">Время</string>
|
||||
<string name="settings_recording_default_activity_title">Вид активности</string>
|
||||
<string name="settings_recording_location_frequency_summary">Записывать точки с интервалом %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Макс. расстояние между точками в одном сегменте: %1$s</string>
|
||||
<string name="settings_chart_by_distance">По расстоянию</string>
|
||||
<string name="settings_chart_by_time">По времени</string>
|
||||
<string name="settings_recording_default_activity_title">Вид активности по умолчанию</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s между записанными точками местоположения</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Новый сегмент создается, если расстояние между двумя точками больше %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Макс. расстояние</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Интервал расстояния</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Интервал времени</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Интервал записи расстояния</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Интервал записи времени</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">Точность GPS</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Дата (ISO 8601)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Дата (по умолчанию)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Дата (местный формат)</string>
|
||||
<string name="settings_recording_track_name_number_option">Номер</string>
|
||||
<string name="settings_recording_track_name_title">Название по умолчанию</string>
|
||||
<string name="settings_recording_track_name_title">Название трека по умолчанию</string>
|
||||
<!-- Settings Reset -->
|
||||
<string name="settings_reset_confirm_message">Будут восстановлены значения по умолчанию.</string>
|
||||
<string name="settings_reset_confirm_message">Все настройки будут сброшены к значениям по умолчанию. Это не удалит треки на устройстве.</string>
|
||||
<string name="settings_reset_confirm_title">Сбросить настройки?</string>
|
||||
<string name="settings_reset_done">Настройки по умолчанию восстановлены</string>
|
||||
<string name="settings_reset_done">Все настройки сброшены к значениям по умолчанию</string>
|
||||
<!-- Settings Sensor -->
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_rate_title">Предпочтение</string>
|
||||
<string name="settings_stats_units_imperial">Британская (мили, футы)</string>
|
||||
<string name="settings_stats_units_metric">Метрическая (км, м)</string>
|
||||
<string name="settings_stats_units_title">Система измерений</string>
|
||||
<string name="settings_stats_rate_title">Предпочитаемая частота</string>
|
||||
<string name="settings_stats_units_imperial">Британская система (мили, футы)</string>
|
||||
<string name="settings_stats_units_metric">Метрическая система (км, м)</string>
|
||||
<string name="settings_stats_units_title">Предпочитаемые единицы измерений</string>
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file_body">Я думаю этот маршрут покажется вам интересным.
|
||||
\n
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">Хочу поделиться треком</string>
|
||||
<string name="share_track_subject">Хочу поделиться с вами треком</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">Средняя длина шага</string>
|
||||
<string name="stats_average_moving_pace">Средний темп движения</string>
|
||||
<string name="stats_average_moving_speed">Средняя скорость движения</string>
|
||||
<string name="stats_average_pace">Средний шаг</string>
|
||||
<string name="stats_average_pace">Средний темп</string>
|
||||
<string name="stats_average_speed">Средняя скорость</string>
|
||||
<string name="stats_altitude">Высота</string>
|
||||
<string name="stats_fastest_pace">Макс. темп</string>
|
||||
@@ -218,11 +218,11 @@ limitations under the License.
|
||||
<string name="stats_total_time">Общее время</string>
|
||||
<!-- Track -->
|
||||
<string name="track_name_format">Трек %1$d</string>
|
||||
<string name="track_markers">%1$s – маркеры</string>
|
||||
<string name="track_markers">%1$s маркеры</string>
|
||||
<!-- Track Delete -->
|
||||
<string name="track_delete_one_confirm_message">Трек и связанные с ним маркеры будут удалены без возможности восстановления.</string>
|
||||
<string name="track_delete_one_confirm_message">Трек и связанные с ним маркеры будут удалены с устройства без возможности восстановления.</string>
|
||||
<string name="track_delete_one_confirm_title">Удалить трек?</string>
|
||||
<string name="track_delete_multiple_confirm_message">Треки и связанные с ними маркеры будут удалены без возможности восстановления.</string>
|
||||
<string name="track_delete_multiple_confirm_message">Выбранные треки и связанные с ними маркеры будут удалены с устройства без возможности восстановления.</string>
|
||||
<string name="track_delete_progress_message">Удаление…</string>
|
||||
<!-- Track Detail -->
|
||||
<string name="track_detail_stats_tab">Статистика</string>
|
||||
@@ -230,118 +230,118 @@ limitations under the License.
|
||||
<string name="track_edit_activity_type">Вид активности (например, пеший туризм)</string>
|
||||
<string name="track_edit_activity_type_hint">Вид активности</string>
|
||||
<!-- Track List -->
|
||||
<string name="track_list_empty_message">Запишите свой первый трек здесь</string>
|
||||
<string name="track_list_empty_message">Начните записывать ваши приключения здесь</string>
|
||||
<!-- Track Widget -->
|
||||
<!-- Tracks -->
|
||||
<!-- Unit -->
|
||||
<string name="unit_feet">фт</string>
|
||||
<string name="unit_feet">фут</string>
|
||||
<string name="unit_kilometer">км</string>
|
||||
<string name="unit_kilometer_per_hour">км/ч</string>
|
||||
<string name="unit_meter">м</string>
|
||||
<string name="unit_mile">мил.</string>
|
||||
<string name="unit_mile">миля</string>
|
||||
<string name="unit_mile_per_hour">миль/ч</string>
|
||||
<string name="unit_minute_per_kilometer">мин/км</string>
|
||||
<string name="unit_minute_per_mile">мин/мил</string>
|
||||
<string name="unit_minute_per_mile">минут на милю</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet_excellent_gps">%1$d фт (отличный сигнал)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d фт (рекомендуется)</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d футов (отличный сигнал GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d футов (рекомендуется)</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d км/ч</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d м (отличный сигнал)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d м (слабый сигнал)</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d м (отличный сигнал GPS)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d м (слабый сигнал GPS)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d м (рекомендуется)</string>
|
||||
<string name="value_integer_mile_hour">%1$d миль/ч</string>
|
||||
<string name="value_integer_minute">%1$d мин</string>
|
||||
<string name="value_integer_second">%1$d сек</string>
|
||||
<string name="value_none">Нет</string>
|
||||
<string name="value_off">Выкл.</string>
|
||||
<string name="value_smallest_recommended">Мин. время (рекомендуется)</string>
|
||||
<string name="value_smallest_recommended">Минимум (рекомендуется)</string>
|
||||
<!-- Waypoint Type -->
|
||||
<string name="waypoint_type_atm">Банкомат</string>
|
||||
<string name="waypoint_type_bank">Банк</string>
|
||||
<string name="waypoint_type_bar">Бар</string>
|
||||
<string name="waypoint_type_barbecue">Барбекю</string>
|
||||
<string name="waypoint_type_building">Здание</string>
|
||||
<string name="waypoint_type_bus_stop">Автобусная остановка</string>
|
||||
<string name="waypoint_type_cafe">Кафе</string>
|
||||
<string name="waypoint_type_campfire">Костер бивачный</string>
|
||||
<string name="waypoint_type_campground">Палаточный лагерь</string>
|
||||
<string name="waypoint_type_church">Церковь</string>
|
||||
<string name="waypoint_type_coffee_house">Кофейня</string>
|
||||
<string name="waypoint_type_convenience_store">Круглосуточный магазин</string>
|
||||
<string name="waypoint_type_drinking_water">Питьевая вода</string>
|
||||
<string name="waypoint_type_falling_rocks">Камнепад</string>
|
||||
<string name="waypoint_type_ferry">Паром</string>
|
||||
<string name="waypoint_type_fire_place">Костер</string>
|
||||
<string name="waypoint_type_fire_station">Пожарная часть</string>
|
||||
<string name="waypoint_type_fishing">Рыбалка</string>
|
||||
<string name="waypoint_type_atm">банкомат</string>
|
||||
<string name="waypoint_type_bank">банк</string>
|
||||
<string name="waypoint_type_bar">бар</string>
|
||||
<string name="waypoint_type_barbecue">барбекю</string>
|
||||
<string name="waypoint_type_building">здание</string>
|
||||
<string name="waypoint_type_bus_stop">автобусная остановка</string>
|
||||
<string name="waypoint_type_cafe">кафе</string>
|
||||
<string name="waypoint_type_campfire">костер (лагерь)</string>
|
||||
<string name="waypoint_type_campground">палаточный лагерь</string>
|
||||
<string name="waypoint_type_church">церковь</string>
|
||||
<string name="waypoint_type_coffee_house">кофейня</string>
|
||||
<string name="waypoint_type_convenience_store">круглосуточный магазин</string>
|
||||
<string name="waypoint_type_drinking_water">питьевая вода</string>
|
||||
<string name="waypoint_type_falling_rocks">камнепад</string>
|
||||
<string name="waypoint_type_ferry">паром</string>
|
||||
<string name="waypoint_type_fire_place">место для костра</string>
|
||||
<string name="waypoint_type_fire_station">пожарная часть</string>
|
||||
<string name="waypoint_type_fishing">рыбалка</string>
|
||||
<string name="waypoint_type_gas_station">АЗС</string>
|
||||
<string name="waypoint_type_golf_course">Поле для гольфа</string>
|
||||
<string name="waypoint_type_grocery_store">Продовольственный магазин</string>
|
||||
<string name="waypoint_type_hair_dresser">Парикмахерская</string>
|
||||
<string name="waypoint_type_harbor">Гавань</string>
|
||||
<string name="waypoint_type_hiking_path">Маршрут пешего похода</string>
|
||||
<string name="waypoint_type_hotel">Гостиница</string>
|
||||
<string name="waypoint_type_house">Дом</string>
|
||||
<string name="waypoint_type_information">Сведения</string>
|
||||
<string name="waypoint_type_intersection">Перекресток</string>
|
||||
<string name="waypoint_type_mail_box">Почтовый ящик</string>
|
||||
<string name="waypoint_type_market">Рынок</string>
|
||||
<string name="waypoint_type_meeting_point">Место встречи</string>
|
||||
<string name="waypoint_type_opera">Опера</string>
|
||||
<string name="waypoint_type_parking">Парковка</string>
|
||||
<string name="waypoint_type_police_station">Отделение полиции</string>
|
||||
<string name="waypoint_type_post_office">Почтовое отделение</string>
|
||||
<string name="waypoint_type_pothole">Выбоина</string>
|
||||
<string name="waypoint_type_railway_crossing">Железнодорожный переезд</string>
|
||||
<string name="waypoint_type_restaurant">Ресторан</string>
|
||||
<string name="waypoint_type_shopping">Торговое заведение</string>
|
||||
<string name="waypoint_type_sight">Достопримечательность</string>
|
||||
<string name="waypoint_type_store">Магазин</string>
|
||||
<string name="waypoint_type_swimming_pool">Бассейн</string>
|
||||
<string name="waypoint_type_telephone">Телефон</string>
|
||||
<string name="waypoint_type_theater">Театр</string>
|
||||
<string name="waypoint_type_toilet">Туалет</string>
|
||||
<string name="waypoint_type_tourist_information">Информация для туристов</string>
|
||||
<string name="waypoint_type_tree">Дерево</string>
|
||||
<string name="waypoint_type_tunnel">Тоннель</string>
|
||||
<string name="waypoint_type_waterfall">Водопад</string>
|
||||
<string name="waypoint_type_water_fountain">Фонтан</string>
|
||||
<string name="waypoint_type_golf_course">поле для гольфа</string>
|
||||
<string name="waypoint_type_grocery_store">продовольственный магазин</string>
|
||||
<string name="waypoint_type_hair_dresser">парикмахерская</string>
|
||||
<string name="waypoint_type_harbor">пристань (порт)</string>
|
||||
<string name="waypoint_type_hiking_path">маршрут для похода</string>
|
||||
<string name="waypoint_type_hotel">гостиница</string>
|
||||
<string name="waypoint_type_house">дом</string>
|
||||
<string name="waypoint_type_information">информация</string>
|
||||
<string name="waypoint_type_intersection">перекресток</string>
|
||||
<string name="waypoint_type_mail_box">почтовый ящик</string>
|
||||
<string name="waypoint_type_market">рынок</string>
|
||||
<string name="waypoint_type_meeting_point">место встречи</string>
|
||||
<string name="waypoint_type_opera">опера</string>
|
||||
<string name="waypoint_type_parking">парковка</string>
|
||||
<string name="waypoint_type_police_station">отдел полиции</string>
|
||||
<string name="waypoint_type_post_office">почтовое отделение</string>
|
||||
<string name="waypoint_type_pothole">выбоина</string>
|
||||
<string name="waypoint_type_railway_crossing">железнодорожный переезд</string>
|
||||
<string name="waypoint_type_restaurant">ресторан</string>
|
||||
<string name="waypoint_type_shopping">торговый центр</string>
|
||||
<string name="waypoint_type_sight">вид</string>
|
||||
<string name="waypoint_type_store">магазин</string>
|
||||
<string name="waypoint_type_swimming_pool">бассейн</string>
|
||||
<string name="waypoint_type_telephone">телефон</string>
|
||||
<string name="waypoint_type_theater">театр</string>
|
||||
<string name="waypoint_type_toilet">туалет</string>
|
||||
<string name="waypoint_type_tourist_information">информация для туристов</string>
|
||||
<string name="waypoint_type_tree">дерево</string>
|
||||
<string name="waypoint_type_tunnel">тоннель</string>
|
||||
<string name="waypoint_type_waterfall">водопад</string>
|
||||
<string name="waypoint_type_water_fountain">фонтан</string>
|
||||
<string name="waypoint_type_waypoint">путевая точка</string>
|
||||
<string name="help_track_map_content"> Используйте маленький значок карты.
|
||||
\na) (живые обновления) Установите <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Он предоставляет карты в режиме онлайн или оффлайн с помощью <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
\nНа самом деле, OSMDashboard — это отдельное приложение, которое может получить доступ (временно) только к данным треков, которые будут показаны.
|
||||
\nb) Через обычное картографическое приложение (например, <a href="http://osmand.net/">OsmAnd</a> и <a href="https://maps.me/">MAPS.ME</a>). Эти приложения должны поддерживать формат файлов KMZ (только местоположения и метки времени). Фактически, приложение должно поддерживать <i>ACTION_VIEW</i> с MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||
<string name="help_storage_pictures_title">Как OpenTracks хранит изображения, сделанные в качестве маркеров\?</string>
|
||||
\nа) (обновление в реальном времени) Установите <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. В нем доступны онлайн или оффлайн карты <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
|
||||
\nНа самом деле, OSMDashboard — это отдельное приложение, которое может получить (временный) доступ только к данным трека(-ов), которые будут просмотрены.
|
||||
\nб) Через обычное картографическое приложение (например, <a href="http://osmand.net/">OsmAnd</a> и <a href="https://maps.me/">MAPS.ME</a>). Такие приложения должны поддерживать формат файлов KMZ (только точки местоположения и метки времени). Фактически, приложение должно поддерживать <i>ACTION_VIEW</i> с MIME <i>application/vnd.google-earth.kmz</i>. </string>
|
||||
<string name="help_storage_pictures_title">Как OpenTracks хранит изображения, используемые в качестве маркеров?</string>
|
||||
<string name="about_preference_title">Об OpenTracks</string>
|
||||
<string name="about_description"><i>OpenTracks</i> — это приложение для отслеживания спортивных событий, которое полностью уважает вашу конфиденциальность.</string>
|
||||
<string name="about_description"><i>OpenTracks</i> — это приложение для отслеживания спортивных тренировок, которое полностью уважает вашу конфиденциальность.</string>
|
||||
<string name="about_url">Веб-страница проекта: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Название версии: %1$s</string>
|
||||
<string name="about_version_code">Код версии: %1$d</string>
|
||||
<string name="about_history">OpenTracks — это форк <a href="https://code.google.com/archive/p/mytracks/"><i>Google MyTracks</i></a> (разработка прекращена в 2016 г.).</string>
|
||||
<string name="about_libraries">Библиотеки</string>
|
||||
<string name="help_feature_title">OpenTracks имеет следующие возможности:</string>
|
||||
<string name="help_feature_title">OpenTracks предоставляет следующие возможности:</string>
|
||||
<string name="help_feature_content">* создание GPS-треков
|
||||
\n* использование датчика частоты пульса (только Bluetooth LE)
|
||||
\n* голосовые повещения (по времени или расстоянию)
|
||||
\n* использование пульсометра (только Bluetooth LE)
|
||||
\n* голосовые оповещения (по времени или расстоянию)
|
||||
\n* создание маркеров (в т.ч. с изображениями)
|
||||
\n* работа полностью офлайн
|
||||
\n* показ треков на карте (см. ниже)</string>
|
||||
<string name="help_track_map_title">Как показать трек на карте\?</string>
|
||||
<string name="help_storage_pictures_content">Они хранятся во внутренней памяти приложения (т. е. доступны только в OpenTracks).</string>
|
||||
<string name="help_track_export_title">Как я могу экспортировать/поделиться записанными треками\?</string>
|
||||
\n* просмотр треков на карте (см. ниже)</string>
|
||||
<string name="help_track_map_title">Как просмотреть трек на карте?</string>
|
||||
<string name="help_storage_pictures_content">Они хранятся во внутренней памяти приложения (т.е. доступны только в OpenTracks).</string>
|
||||
<string name="help_track_export_title">Как я могу экспортировать записанные треки или поделиться ими?</string>
|
||||
<string name="help_data_google_play_services_title">Требует/использует ли OpenTracks сервисы Google Play\?</string>
|
||||
<string name="help_recording_title">Как OpenTracks записывает данные\?</string>
|
||||
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> в формате KMZ. </string>
|
||||
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string>
|
||||
<string name="export_progress_summary_new_msg">Новых треков экспортировано</string>
|
||||
<string name="help_track_export_content"> Треки могут быть экспортированы в Настройках в форматах KMZ, KML или GPX (KMZ/KML - все данные, GPX - только точки местоположения). При использовании <i>значка \"Поделиться\"</i> трек отправляется в формате KMZ. </string>
|
||||
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если GPS датчик устройства сообщает неточные данные (о, например, местоположении, скорости, высоте), OpenTracks ничего не может с этим поделать.</string>
|
||||
<string name="export_progress_summary_new_msg">Новых файлов экспортировано</string>
|
||||
<string name="help_data_storage_title">Как OpenTracks записывает данные с датчиков\?</string>
|
||||
<string name="export_track_already_exists_msg">Дорожка %1$s уже существует в каталоге назначения.</string>
|
||||
<string name="export_track_already_exists_msg">Файл %1$s уже существует в папке назначения.</string>
|
||||
<string name="export_without_photos">без фотографий</string>
|
||||
<string name="export_with_photos">с фотографией</string>
|
||||
<string name="export_with_photos">с фотографиями</string>
|
||||
<string name="description_default_speed_or_pace">По типу деятельности</string>
|
||||
<string name="export_all_do_it_for_all">для всех конфликтов</string>
|
||||
<string name="stats_loss">Спуск</string>
|
||||
<string name="export_progress_summary_skip_msg">Пропущенные треки</string>
|
||||
<string name="export_all_do_it_for_all">выполнить для всех конфликтов</string>
|
||||
<string name="stats_loss">Потеря высоты</string>
|
||||
<string name="export_progress_summary_skip_msg">Файлы пропущены</string>
|
||||
<string name="generic_click_twice_cancel">Нажмите назад еще раз, чтобы отменить</string>
|
||||
<string name="generic_completed">Завершено</string>
|
||||
<string name="generic_show_errors">Показать ошибки</string>
|
||||
@@ -354,47 +354,47 @@ limitations under the License.
|
||||
<string name="import_error_info">%1$s: %2$s</string>
|
||||
<string name="import_no_kml_file_found">Файл doc.kml не найден в KMZ</string>
|
||||
<string name="import_unable_to_import_file">Невозможно импортировать файл: %1$s</string>
|
||||
<string name="import_parser_error">Ошибка парсера: %1$s</string>
|
||||
<string name="import_unsupported_format">Недопустимый формат файла</string>
|
||||
<string name="import_thread_interrupted">Нить прервана</string>
|
||||
<string name="gps_fixed_and_ready">GPS зафиксирован и готов</string>
|
||||
<string name="import_parser_error">Ошибка обработки: %1$s</string>
|
||||
<string name="import_unsupported_format">Неподдерживаемый формат файла</string>
|
||||
<string name="import_thread_interrupted">Поток прерван</string>
|
||||
<string name="gps_fixed_and_ready">GPS-связь установлена и готова</string>
|
||||
<string name="gps_disabled_msg">GPS отключен</string>
|
||||
<string name="marker_add_photo_canceled">Отменено, фотомаркер не добавлен</string>
|
||||
<string name="import_progress_summary_exists_msg">Уже существует</string>
|
||||
<string name="import_progress_summary_ok_msg">Импорт файлов</string>
|
||||
<string name="import_progress_summary_ok_msg">Файлы импортированы</string>
|
||||
<string name="menu_resume_track">Возобновить трек</string>
|
||||
<string name="settings_night_mode_title">Тема интерфейса</string>
|
||||
<string name="open_track_as_trackfileformat">Открыть как %1$s</string>
|
||||
<string name="help_support_title">Как поддержать OpenTracks\?</string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks не регистрирует местоположения\?</string>
|
||||
<string name="activity_type_escooter">Электросамокат</string>
|
||||
<string name="permission_gps_failed">OpenTracks требует разрешения на использование GPS.</string>
|
||||
<string name="help_support_title">Как я могу поддержать OpenTracks?</string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks не записывает точки местоположения?</string>
|
||||
<string name="activity_type_escooter">электросамокат</string>
|
||||
<string name="permission_gps_failed">OpenTracks требуется разрешение на использование GPS.</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Предотвращение повторного импорта треков</string>
|
||||
<string name="sensor_state_cadence_max">Макс. каденция</string>
|
||||
<string name="sensor_state_cadence_max">Макс. каденс</string>
|
||||
<string name="sensor_state_cadence_avg">Средний каденс</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_title">Полный экран</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Во время записи не выключайте экран.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Не выключайте экран</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Во время записи не выключать экран.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Не выключать экран</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показывать статистику на экране блокировки</string>
|
||||
<string name="settings_default_trackfileformat">Формат экспорта/обмена файлами</string>
|
||||
<string name="sensor_state_heart_rate_max">Максимальный пульс</string>
|
||||
<string name="settings_default_trackfileformat">Формат файла для экспорта/обмена</string>
|
||||
<string name="sensor_state_heart_rate_max">Макс. пульс</string>
|
||||
<string name="sensor_state_heart_rate_avg">Средний пульс</string>
|
||||
<string name="bluetooth_disabled">Включите Bluetooth.</string>
|
||||
<string name="sensor_state_power_avg">Средняя мощность</string>
|
||||
<string name="track_recording_notification_accuracy">Точность расположения: %1$s</string>
|
||||
<string name="track_recording_notification_accuracy">Точность местоположения: %1$s</string>
|
||||
<string name="stats_split_interval">Размер интервалов</string>
|
||||
<string name="settings_sensor_wheel_circumference">Окружность колеса (мм)</string>
|
||||
<string name="settings_night_mode_option_night">Ночная</string>
|
||||
<string name="settings_night_mode_option_day">Дневная</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Перезапись треков</string>
|
||||
<string name="export_track_errors">Треки не экспортированы</string>
|
||||
<string name="export_error_post_workout">Сбой экспорта после тренировки, пожалуйста, проверьте каталог экспорта.</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Файлы перезаписаны</string>
|
||||
<string name="export_track_errors">Файлы не экспортированы</string>
|
||||
<string name="export_error_post_workout">Сбой экспорта после тренировки, пожалуйста, проверьте папку экспорта.</string>
|
||||
<string name="description_altitude_loss">Потеря высоты: %1$d м (%2$d футов)</string>
|
||||
<string name="settings_default_export_uri_title">Каталог экспорта треков</string>
|
||||
<string name="settings_default_export_uri_title">Папка для экспорта треков</string>
|
||||
<string name="instant_export_enabled_summary">Экспортировать трек в хранилище после завершения записи</string>
|
||||
<string name="share_image_subject">Я хотел бы поделиться с вами изображением</string>
|
||||
<string name="track_detail_intervals_tab">Интервалы</string>
|
||||
<string name="help_data_storage_content"> Данные датчиков сохраняются для каждого местоположения. Для датчика пульса это означает, что для местоположения сохраняется только <i>одно</i> значение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записываться (но будут отображаться). </string>
|
||||
<string name="help_data_storage_content"> Данные датчиков сохраняются для каждого записанного местоположения. Для пульсометра это означает, что на каждую точку местоположения сохраняется только <i>одно</i> значение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записываться (но будут отображаться). </string>
|
||||
<string name="help_gps_troubleshooting_content"> Что можно сделать:
|
||||
\n a) проверить, включен ли GPS,
|
||||
\n б) улучшить прием GPS (например, выйти из помещения),
|
||||
@@ -406,7 +406,7 @@ limitations under the License.
|
||||
<string name="track_speed_notification">Скорость: %1$s</string>
|
||||
<string name="track_distance_notification">Расстояние: %1$s</string>
|
||||
<string name="interval_list_empty_message">Интервалов пока нет</string>
|
||||
<string name="aggregated_stats_empty_message">Запишите свой первый трек, чтобы увидеть собранную статистику</string>
|
||||
<string name="aggregated_stats_empty_message">Запишите свой первый трек, чтобы увидеть статистику</string>
|
||||
<string name="instant_export_enabled_title">Мгновенный экспорт после тренировки</string>
|
||||
<string name="hold_to_stop">Удерживайте для остановки</string>
|
||||
<string name="share_image_body">Я думаю, вас может заинтересовать это изображение.</string>
|
||||
@@ -414,14 +414,14 @@ limitations under the License.
|
||||
\n a) распространять информацию об OpenTracks,
|
||||
\n б) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">предлагать возможности</a>,
|
||||
\n в) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">сообщать об ошибках</a>,
|
||||
\n d) помогать в разработке программного обеспечения,
|
||||
\n д) покупать в <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
|
||||
\n f) жертвовать, и
|
||||
\n g) делать все, что, по вашему мнению, поможет OpenTracks. </string>
|
||||
\n d) помочь в написании кода приложения,
|
||||
\n д) купить приложение в <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
|
||||
\n f) пожертвовать денежные средства, и
|
||||
\n g) сделать что-то другое, что, по вашему мнению, поможет OpenTracks. </string>
|
||||
<string name="settings_night_mode_option_system">Системная</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Во время записи присутствует полноэкранный режим.</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Во время записи показывайте статистику, не разблокируя устройство.</string>
|
||||
<string name="menu_insert_gallery_img">Вставка изображения галереи</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Во время записи показывать в полноэкранном режиме.</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Во время записи показывать статистику без разблокировки устройства.</string>
|
||||
<string name="menu_insert_gallery_img">Добавить изображение из галереи</string>
|
||||
<string name="sensor_could_not_scan">Не удалось выполнить сканирование устройств Bluetooth (ошибка %1$i).</string>
|
||||
<string name="import_progress_summary_errors_msg">Ошибки</string>
|
||||
<string name="generic_overwrite">Перезаписать</string>
|
||||
@@ -430,64 +430,64 @@ limitations under the License.
|
||||
<string name="menu_voice_rate">Скорость голоса</string>
|
||||
<string name="track_delete_not_recording">Этот трек нельзя удалить, так как он записывается в данный момент.</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">Завершена с %1$d ошибкой</item>
|
||||
<item quantity="few">Завершено с %1$d ошибками</item>
|
||||
<item quantity="many">Завершено с %1$d ошибок</item>
|
||||
<item quantity="other">Завершено с %1$d ошибок</item>
|
||||
<item quantity="one">Завершено, %1$d ошибка</item>
|
||||
<item quantity="few">Завершено, %1$d ошибки</item>
|
||||
<item quantity="many">Завершено, %1$d ошибок</item>
|
||||
<item quantity="other">Завершено, %1$d ошибок</item>
|
||||
</plurals>
|
||||
<string name="settings_sensor_cycling_cadence">Каденция</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Дистанция и Скорость</string>
|
||||
<string name="settings_sensor_running_speed_and_cadence">Дистанция, Скорость и Каденция</string>
|
||||
<string name="settings_sensor_cycling_cadence">Каденс</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Расстояние и скорость</string>
|
||||
<string name="settings_sensor_running_speed_and_cadence">Расстояние, скорость и каденс</string>
|
||||
<string name="stats_sensors_cadence">Каденс</string>
|
||||
<string name="activity_type_swimming">Плавание</string>
|
||||
<string name="activity_type_swimming_open">Плавание на открытой воде</string>
|
||||
<string name="activity_type_workout">Тренировка</string>
|
||||
<string name="activity_type_swimming">плавание</string>
|
||||
<string name="activity_type_swimming_open">плавание на открытой воде</string>
|
||||
<string name="activity_type_workout">тренировка</string>
|
||||
<string name="description_speed_source_gps">GPS</string>
|
||||
<string name="generic_choose_an_option">Выберите вариант</string>
|
||||
<string name="menu_select_layout">Выберите раскладку</string>
|
||||
<string name="menu_add_custom_layout">Новая раскладка</string>
|
||||
<string name="menu_filter">Фильтр</string>
|
||||
<string name="permission_bluetooth_failed">OpenTracks требует разрешение на использование Bluetooth.</string>
|
||||
<string name="permission_bluetooth_failed_rejected">Вам нужно вручную разрешить OpenTracks доступ к Устройствам поблизости в настройках системы.</string>
|
||||
<string name="permission_bluetooth_failed">OpenTracks требуется разрешение на использование Bluetooth.</string>
|
||||
<string name="permission_bluetooth_failed_rejected">Вам нужно вручную разрешить OpenTracks доступ к информации об устройствах поблизости в настройках системы.</string>
|
||||
<string name="settings_gps_title">GPS</string>
|
||||
<string name="settings_gps_summary">Интервал времени и дистанции, Максимальная дистанция, Точность</string>
|
||||
<string name="settings_gps_summary">Интервал времени, интервал расстояния, макс. расстояние, точность</string>
|
||||
<string name="settings_sensors_title">Bluetooth-датчики</string>
|
||||
<string name="settings_sensors_summary">Датчики велосипеда, Оборот колеса, Датчики бега</string>
|
||||
<string name="settings_announcements_statistics_title">Голосовые подсказки</string>
|
||||
<string name="settings_import_export_title">Импорт/Экспорт</string>
|
||||
<string name="settings_import_export_summary">Импорт, Экспорт, Автоэкспорт</string>
|
||||
<string name="settings_units_category">Единицы</string>
|
||||
<string name="settings_sensors_summary">Велосипедные датчики, окружность колеса, датчики для бега</string>
|
||||
<string name="settings_announcements_statistics_title">Голосовые подсказки статистические</string>
|
||||
<string name="settings_import_export_title">Импорт/экспорт</string>
|
||||
<string name="settings_import_export_summary">Импорт, экспорт, автоэкспорт</string>
|
||||
<string name="settings_units_category">Единицы измерения</string>
|
||||
<string name="settings_announcements_totaltime_frequency">Интервал времени</string>
|
||||
<string name="settings_announcements_distance_frequency">Интервал дистанции</string>
|
||||
<string name="settings_announcements_distance_frequency">Интервал расстояния</string>
|
||||
<string name="custom_layout_list_layout_removed">Раскладка удалена</string>
|
||||
<string name="settings_cycling_sensor">Велосипед</string>
|
||||
<string name="settings_public_api_enabled_title">Публичный API</string>
|
||||
<string name="settings_public_api_enabled_summary_on">Другие приложения могут запускать или останавливать запись.</string>
|
||||
<string name="settings_public_api_enabled_summary_on">Другие установленные приложения могут запускать или останавливать запись.</string>
|
||||
<string name="settings_public_api_enabled_summary_off">Только OpenTracks может запускать и останавливать запись.</string>
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f миль/ч (рекомендуется)</string>
|
||||
<string name="export_cannot_write_to_dir">Невозможно записать в папку экспорта.</string>
|
||||
<string name="settings_open_tracks_summary">Всё о проекте OpenTracks</string>
|
||||
<string name="settings_defaults_summary">Предпочитаемые единицы, активность по умолчанию, название трека</string>
|
||||
<string name="settings_defaults_summary">Предпочитаемые единицы, вид активности и название трека по умолчанию</string>
|
||||
<string name="settings_recording_customize_layout_title">Изменить раскладку записи</string>
|
||||
<string name="settings_recording_customize_layout_select_columns">Поля данных на строку</string>
|
||||
<string name="settings_recording_customize_layout_select_columns">Ячеек с данными в строке</string>
|
||||
<string name="custom_layout_list_title">Раскладки</string>
|
||||
<string name="custom_layout_list_edit_name">Напишите имя раскладки</string>
|
||||
<string name="custom_layout_list_edit_name">Введите название добавляемой раскладки</string>
|
||||
<string name="aggregated_stats_filter_no_results">Нет активностей для данного фильтра</string>
|
||||
<string name="custom_layout_list_edit_already_exists">Имя раскладки уже существует</string>
|
||||
<string name="settings_layout_reset">Сбросить свои раскладки</string>
|
||||
<string name="settings_layout_reset_done">Настройки всех раскладок были сброшены</string>
|
||||
<string name="custom_layout_list_edit_already_exists">Название раскладки уже существует</string>
|
||||
<string name="settings_layout_reset">Сбросить пользовательскую раскладку</string>
|
||||
<string name="settings_layout_reset_done">Настройки всех раскладок были сброшены к значениям по умолчанию</string>
|
||||
<string name="generic_to">по</string>
|
||||
<string name="generic_today">Сегодня</string>
|
||||
<string name="generic_yesterday">Вчера</string>
|
||||
<string name="settings_running_sensor">Бег</string>
|
||||
<string name="stats_clock">Часы</string>
|
||||
<string name="settings_activity_defaults_category">Настройки активности</string>
|
||||
<string name="settings_reset_summary">Сбросить настройки</string>
|
||||
<string name="settings_generic_sensor">Обычные</string>
|
||||
<string name="settings_activity_defaults_category">Настройки активности по умолчанию</string>
|
||||
<string name="settings_reset_summary">Сбросить настройки к значениям по умолчанию</string>
|
||||
<string name="settings_generic_sensor">Без типа</string>
|
||||
<string name="custom_layout_select_layout">Выбрать раскладку</string>
|
||||
<string name="stats_custom_layout_default_layout">Раскладка по умолчанию</string>
|
||||
<string name="settings_layout_reset_confirm_title">Сбросить свои раскладки\?</string>
|
||||
<string name="settings_layout_reset_confirm_message">Настройки всех раскладок будут сброшены на значения по умолчанию. Это не удалит треки на устройстве.</string>
|
||||
<string name="settings_layout_reset_confirm_title">Сбросить ваши раскладки?</string>
|
||||
<string name="settings_layout_reset_confirm_message">Настройки всех раскладок будут сброшены к значениям по умолчанию. Это не удалит треки на устройстве.</string>
|
||||
<string name="stats_sensors_heart_rate">Пульс</string>
|
||||
<string name="stats_sensors_power">Мощность</string>
|
||||
<string name="value_float_mile_hour">%1$.1f миль/ч</string>
|
||||
@@ -496,8 +496,8 @@ limitations under the License.
|
||||
<string name="lap_time">Время круга</string>
|
||||
<string name="pace">Темп</string>
|
||||
<string name="lap_speed">Скорость круга</string>
|
||||
<string name="speed">Скорость</string>
|
||||
<string name="total_distance">Общая дистанция</string>
|
||||
<string name="speed">Средняя скорость в движении</string>
|
||||
<string name="total_distance">Общее расстояние</string>
|
||||
<string name="average_heart_rate">Средний пульс</string>
|
||||
<string name="current_heart_rate">Текущий пульс</string>
|
||||
<string name="generic_filter">Фильтр</string>
|
||||
@@ -509,7 +509,7 @@ limitations under the License.
|
||||
<string name="settings_public_api_dashboard_enabled_title">Автоматическая передача данных</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_on">Приложение, запустившее запись, также может просматривать записанные данные.</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">Записанные данные не будут автоматически доступны другим приложениям.</string>
|
||||
<string name="stats_coordinates">Широта/Долгота</string>
|
||||
<string name="stats_coordinates">Широта/долгота</string>
|
||||
<string name="location_coordinate">%1$s°</string>
|
||||
<string name="location_latitude_longitude">%1$s, %2$s</string>
|
||||
<string name="value_float_kilometer_hour">%1$.1f км/ч</string>
|
||||
@@ -517,37 +517,78 @@ limitations under the License.
|
||||
<string name="field_set_secondary">Дополнительная</string>
|
||||
<string name="field_set_primary">Главная</string>
|
||||
<string name="field_remove_from_layout">Удалить</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_on">Показывать только устройства, объявляющие о требуемых службах</string>
|
||||
<string name="settings_defaults_title">Единицы измерения и активность</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_on">Показывать только устройства Bluetooth, объявляющие о требуемых службах</string>
|
||||
<string name="settings_defaults_title">Единицы измерения и вид активности по умолчанию</string>
|
||||
<string name="settings_ui_title">Пользовательский интерфейс</string>
|
||||
<string name="settings_ui_summary">Вид, Раскладка записи, Тема</string>
|
||||
<string name="export_dir_not_writable">" (недоступно для записи!)"</string>
|
||||
<string name="generic_undo">Отменить</string>
|
||||
<string name="no_compatible_file_manager_installed">Не найден подходящий менеджер файлов</string>
|
||||
<string name="no_compatible_file_manager_installed">Не установлен совместимый менеджер файлов</string>
|
||||
<string name="generic_dismiss">Отклонить</string>
|
||||
<string name="gps_recording_status">%1$s:%2$s</string>
|
||||
<string name="gps_recording_without_signal">OpenTracks записывает без данных GPS.</string>
|
||||
<string name="settings_announcements_summary">Время, Дистанция, Настройки голоса</string>
|
||||
<string name="settings_announcements_summary">Время, расстояние, скорость речи</string>
|
||||
<string name="settings_all_reset">Сброс</string>
|
||||
<string name="settings_export_filename_title">Формат имени файла</string>
|
||||
<string name="error_crash_title">%1$s вылетел</string>
|
||||
<string name="share_crash_report">Поделиться отчётом о вылете</string>
|
||||
<string name="error_crash_title">%1$s произошел сбой</string>
|
||||
<string name="share_crash_report">Поделиться отчетом о сбое</string>
|
||||
<string name="report_bug">Сообщить об ошибке</string>
|
||||
<string name="unit_knots">уз</string>
|
||||
<string name="value_integer_knots">%1$d узлов</string>
|
||||
<string name="value_float_knots">%1$.1f узла</string>
|
||||
<string name="value_float_knots">%1$.1f узлов</string>
|
||||
<string name="image_discard">Отменить</string>
|
||||
<string name="track_discarding">Отмена записи трека</string>
|
||||
<string name="settings_announcements_lap_heart_rate">Значение пульса за круг</string>
|
||||
<string name="settings_announcements_lap_speed_pace">Скорость/темп за круг</string>
|
||||
<string name="settings_public_api_package_title">Имя пакета для публичного API</string>
|
||||
<string name="settings_public_api_package_title">Название пакета для публичного API</string>
|
||||
<string name="description_speed_nautical">Скорость (узлы)</string>
|
||||
<string name="value_float_knots_recommended">%1$.1f узла (рекомендуется)</string>
|
||||
<string name="settings_announcements_average_heart_rate">Средний пульс</string>
|
||||
<string name="settings_stats_units_nautical">Морская (морские мили, фут)</string>
|
||||
<string name="select_show_on_map_behavior">Выберите формат данных для показа на карте</string>
|
||||
<string name="settings_stats_units_nautical">Морская система (морские мили, футы)</string>
|
||||
<string name="select_show_on_map_behavior">Выберите действие для \"Показать на карте\"</string>
|
||||
<string name="show_on_map_format_ask">Спрашивать</string>
|
||||
<string name="always">Запомнить</string>
|
||||
<string name="always">Всегда</string>
|
||||
<string name="voice_per_nautical_mile">за морскую милю</string>
|
||||
<string name="settings_announcements_average_speed_pace">Средняя скорость/темп</string>
|
||||
<string name="unit_minute_per_nautical_mile">минут на морскую милю</string>
|
||||
<string name="sensor_state_heart_rate_unit">уд/мин</string>
|
||||
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 nautical mile} other {{n,number,#.0} nautical miles} }</string>
|
||||
<string name="voiceMinutesPlural">{n, plural, =1 {1 minute} other {{n,number} minutes} }</string>
|
||||
<string name="help_data_google_play_services_content">Не использует</string>
|
||||
<string name="menu_export_all_one_file">Экспортировать все треки в один файл</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d треки</string>
|
||||
<string name="show_on_dashboard">OpenTracks Dashboard API</string>
|
||||
<string name="settings_announcements_idle_category_title">Голосовые подсказки без движения</string>
|
||||
<string name="sensor_state_power_unit">Вт</string>
|
||||
<string name="settings_stats_units_imperial_meter">Британская система (мили, метры)</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 mile per hour} other {{n,number,#.0} miles per hour} }</string>
|
||||
<string name="voiceDistanceMilesPlural">{n, plural, =1 {1 mile} other {{n,number,#.0} miles} }</string>
|
||||
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {1 kilometer per hour} other {{n,number,#.0} kilometers per hour} }</string>
|
||||
<string name="voice_on_device_speaker_title">Использовать динамик устройства</string>
|
||||
<string name="settings_ui_dynamic_colors_title">Динамические цвета</string>
|
||||
<string name="settings_sensor_heart_rate_max">Зоны: макс. пульс (уд/мин)</string>
|
||||
<string name="settings_recording_title">Настройки записи</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">Использовать пользовательские динамические цвета (только Android 12 и новее; требуется перезапуск вручную)</string>
|
||||
<string name="description_pace_nautical">Темп (минут на морскую милю)</string>
|
||||
<string name="sensor_state_power_max">Макс. мощность</string>
|
||||
<string name="activity_type_kickscooter">самокат</string>
|
||||
<string name="no_compatible_camera_installed">Не установлено совместимое приложение камеры</string>
|
||||
<string name="voiceSecondsPlural">{n, plural, =1 {1 second} other {{n,number} seconds} }</string>
|
||||
<string name="settings_api_summary">API для удаленного управления и статистики для сторонних приложений</string>
|
||||
<string name="no_compatible_gallery_installed">Не установлено совместимое приложение для просмотра изображений</string>
|
||||
<string name="show_on_dashboard_not_installed">Не установлено приложение, поддерживающее OpenTracks Dashboard API.</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">Показывать все видимые устройства Bluetooth</string>
|
||||
<string name="unit_nautical_mile">морские мили</string>
|
||||
<string name="permission_recording_failed">OpenTracks требуется разрешение на доступ к информации о вашем местоположении и устройствах поблизости.</string>
|
||||
<string name="sensor_state_cadence_unit">об/мин</string>
|
||||
<string name="voiceIdle">Переход в режим без движения.</string>
|
||||
<string name="introduction_osm_dashboard">В OpenTracks не предусмотрена карта. Пожалуйста, установите OSMDashboard для просмотра ваших записей на карте.</string>
|
||||
<string name="voiceHoursPlural">{n, plural, =1 {1 hour} other {{n,number} hours} }</string>
|
||||
<string name="settings_announcements_idle_title">Без движения</string>
|
||||
<string name="settings_recording_idle_timeout_title">Предел периода без движения</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Информация о местоположении игнорируется, если показатель точности превышает %1$s</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 knot} other {{n,number,#.0} knots} }</string>
|
||||
<string name="value_int_seconds">%1$d сек (рекомендуется)</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Фильтровать устройства Bluetooth</string>
|
||||
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {1 kilometer} other {{n,number,#.0} kilometers} }</string>
|
||||
</resources>
|
||||
@@ -587,4 +587,5 @@ limitations under the License.
|
||||
<string name="value_int_seconds">%1$d s (khuyến khích)</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Lọc thiết bị Bluetooth</string>
|
||||
<string name="voiceDistanceKilometersPlural">{n, số nhiều, =1 {1 kilômet} khác {{n,số,#.0} kilômet} }</string>
|
||||
<string name="permission_recording_failed">OpenTracks yêu cầu quyền truy cập vào vị trí của bạn cũng như các thiết bị lân cận.</string>
|
||||
</resources>
|
||||
@@ -604,4 +604,5 @@ limitations under the License.
|
||||
<string name="settings_ui_dynamic_colors_title">動態色彩</string>
|
||||
<string name="settings_recording_title">紀錄</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">使用使用者自訂的動態色彩(僅限 Android 12+;需要手動重新啟動)</string>
|
||||
<string name="permission_recording_failed">OpenTracks 需要授權以存取您的位置以及附近的裝置。</string>
|
||||
</resources>
|
||||
@@ -616,4 +616,15 @@ limitations under the License.
|
||||
<string name="sensor_state_power_unit">瓦</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_off">显示所有可发现的蓝牙设备</string>
|
||||
<string name="help_data_google_play_services_content">否</string>
|
||||
<string name="settings_announcements_idle_category_title">空闲公告</string>
|
||||
<string name="settings_ui_dynamic_colors_title">动态颜色</string>
|
||||
<string name="settings_recording_title">记录</string>
|
||||
<string name="settings_ui_dynamic_colors_summary">用户自定动态颜色(需要安卓12或更高版本,需要手动重启App)</string>
|
||||
<string name="sensor_state_power_max">最大功率</string>
|
||||
<string name="activity_type_kickscooter">滑板车</string>
|
||||
<string name="permission_recording_failed">OpenTracks 需要获取定位和查找附近设备权限。</string>
|
||||
<string name="voiceIdle">即将进入空闲状态。</string>
|
||||
<string name="settings_announcements_idle_title">空闲</string>
|
||||
<string name="settings_recording_idle_timeout_title">空闲阈值</string>
|
||||
<string name="value_int_seconds">%1$d 秒(推荐)</string>
|
||||
</resources>
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_announcements_idle_category_title">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/voice_announce_total_distance_default"
|
||||
android:key="@string/voice_announce_total_distance_key"
|
||||
android:defaultValue="@bool/voice_announce_idle_default"
|
||||
android:key="@string/voice_announce_idle_key"
|
||||
android:title="@string/settings_announcements_idle_title" />
|
||||
</PreferenceCategory>
|
||||
|
||||
@@ -70,4 +70,4 @@
|
||||
android:title="@string/settings_announcements_lap_heart_rate" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user