forked from upstream-mirrors/OpenTracks
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3075f4010f | |||
| 82e305e6a7 | |||
| 6351a335c0 | |||
| 6538daedef | |||
| 82fe859c84 | |||
| f567b03be5 | |||
| 7dc4e8dbc8 | |||
| 2a272a887e | |||
| 868509e00b | |||
| 477bdd5214 | |||
| 286cec8d77 | |||
| 58d4ee7d7d | |||
| 0e93a304f0 | |||
| 0adc4c7986 | |||
| a93424629d | |||
| da29766fd0 | |||
| dc7afbd07f | |||
| 67b174feac | |||
| b6b81b5d3d | |||
| 7d804b603d | |||
| 8696a15ce7 | |||
| 989fe39849 | |||
| ed1e432691 | |||
| 4f612e32d6 | |||
| e5d1e421a4 | |||
| 08ab8cc61d | |||
| c3b30815c8 | |||
| 5a4919efd2 | |||
| bb549fdc93 | |||
| 73e59cbba2 | |||
| 27727b6dcf | |||
| 06d2957407 | |||
| 8b08af6bcf | |||
| 78dad95ef6 | |||
| be9d56721e | |||
| e5785dd3ac | |||
| 11d912d14d | |||
| 807ac3fc9f | |||
| 45874d3749 | |||
| 594f69320a | |||
| d4927c9444 | |||
| 00dbd056d6 | |||
| 8f19ad8f66 | |||
| 2fdad54a7c | |||
| 5a5f040ed5 | |||
| 0ada9a2463 | |||
| 719bb981ec | |||
| 3c95d8744d | |||
| 63d1c9ee5e |
@@ -64,19 +64,19 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
||||
* __Voice announcements__
|
||||
* __Photos and Markers:__ mark interesting locations while tracking
|
||||
* __Export:__
|
||||
* export tracks either as KMZ (incl. photos), KML, or GPX
|
||||
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* export tracks either as [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) (incl. photos), [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html), or [GPX 1.1](https://www.topografix.com/GPX/1/1/)
|
||||
* export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/))
|
||||
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
|
||||
* __Altitude:__
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* gain/loss via barometric sensor (if present)
|
||||
* shown in EGM2008 (above mean sea level); exported as WGS84
|
||||
* __Bluetooth LE sensors:__
|
||||
* heart rate
|
||||
* cycling: speed and distance
|
||||
* cycling: cadence
|
||||
* cycling: power meter
|
||||
* running: speed and cadence
|
||||
* support for BLE sensor training only (i.e., without GPS) for indoor training
|
||||
* heart rate
|
||||
* cycling: speed and distance
|
||||
* cycling: cadence
|
||||
* cycling: power meter
|
||||
* running: speed and cadence
|
||||
* support for BLE sensor training only (i.e., without GPS) for indoor training
|
||||
|
||||
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)
|
||||
|
||||
@@ -111,19 +111,24 @@ The API can be invoked by sending an explicit Intent to start an activity.
|
||||
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
|
||||
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
|
||||
|
||||
For testing via adb: `adb shell am start -n "package/class"`
|
||||
For testing via adb: `adb shell am start -e someParameter someValue -n "package/class"`
|
||||
|
||||
`StartRecording` supports the following parameters:
|
||||
|
||||
* Set track data: `TRACK_NAME`, `TRACK_CATEGORY`, and `TRACK_DESCRIPTION`
|
||||
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE`
|
||||
and `STATS_TARGET_CLASS`
|
||||
* Set track data: `TRACK_NAME`, `TRACK_DESCRIPTION`, `TRACK_CATEGORY`, and `TRACK_ICON` (non-localized identifier see [/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38](/src/main/java/de/dennisguse/opentracks/util/TrackIconUtils.java#L38)).
|
||||
NOTE: if `TRACK_ICON` is not present, `TRACK_CATEGORY` will be used to determine the icon (localized).
|
||||
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE` and `STATS_TARGET_CLASS`
|
||||
|
||||
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in
|
||||
the settings.
|
||||
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
|
||||
|
||||
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or
|
||||
Bluetooth) were granted. If they are not granted`
|
||||
## File formats compatibility with open-source software
|
||||
|
||||
| | [GPX 1.1](https://www.topografix.com/GPX/1/1/) | [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) |
|
||||
|------------------------------------------------------|------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------|
|
||||
| [OpenLayers 7.1.0](https://openlayers.org/) | ? | [no](https://github.com/openlayers/openlayers/issues/14104) | [no](https://github.com/openlayers/openlayers/issues/14104) |
|
||||
| [Golden Cheetah 3.5](https://www.goldencheetah.org/) | ? | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) |
|
||||
| [GpxPod](https://apps.nextcloud.com/apps/gpxpod) | ? | ? | ? |
|
||||
| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) |
|
||||
|
||||
## Dashboard API (incl. map)
|
||||
|
||||
|
||||
+11
-6
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4993
|
||||
versionName "v4.0.6"
|
||||
versionCode 5042
|
||||
versionName "v4.1.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -81,6 +81,8 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'de.dennisguse.opentracks'
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
@@ -118,20 +120,23 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.6.1'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.8.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.0.7: OpenTracks
|
||||
|
||||
Changes:
|
||||
- GPS signal lost timeout: 30s
|
||||
- Android13: request permission to show notifications
|
||||
- Faster deletion of tracks (vacuum database only if needed)
|
||||
|
||||
Bugfix:
|
||||
- PublicAPI stop command resulted in crash (introduced in v4.0.6)
|
||||
- Permissions: request all permissions during on startup
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
v4.0.8: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Altitude always shown in m/ft
|
||||
- Show track timezone if different than device timezone
|
||||
- PublicAPI: support setting the track icon explicitly
|
||||
|
||||
Developer:
|
||||
- Introduced DistanceFormatter
|
||||
- Upgrade to AGP 7.3/desugar for Duration.class
|
||||
- Android12+: do splashscreen via Androidx
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.1.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Export KML/KMZ version 2.3 only (was 2.2)
|
||||
- For crank-based powermeters: fetch cadence
|
||||
|
||||
Bugfix:
|
||||
- Workaround for crash on "import all"
|
||||
@@ -1,11 +1,22 @@
|
||||
OpenTracks是一个完全尊重你的隐私的运动跟踪应用程序。
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
|
||||
健康幸福的生活总少不了各种运动和户外活动。
|
||||
|
||||
使用本应用,您可以保持对您的训练数据的有效追踪,为您的健康助力。
|
||||
本应用可以在您跑步或散步时记录数据,也可以在您骑车时作为一个大屏幕自行车电脑。
|
||||
您可以为路上的有趣位置创建标记,还可以用图片作为标记。
|
||||
您的数据会被详尽地记录下来,供您查阅分析。
|
||||
您的数据只会分享给您想要分享给的人。
|
||||
|
||||
特点。
|
||||
* 运动追踪:追踪你的运动和户外活动
|
||||
* 照片和标记:在跟踪时标记有趣的地点
|
||||
* 海拔增减:通过气压传感器
|
||||
* 高度显示为EGM2008(平均海平面以上);导出为WGS84
|
||||
* 语音播报
|
||||
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、节奏(骑车)和功率计(骑车)。
|
||||
* 导出数据:以KMZ(包括照片)、KML或GPX形式导出轨迹
|
||||
* 没有互联网接入。不使用互联网
|
||||
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、踏频(骑车)和功率计(骑车)。
|
||||
* 海拔增减:通过气压传感器。
|
||||
* EGM2008 海拔显示(平均海平面以上);导出为 WGS84。
|
||||
* 导出路线数据,支持 KMZ(包括照片)、KML 和 GPX 格式。
|
||||
* 不需要联网,不需要额外权限。
|
||||
* 深色和浅色主题,跟随系统设置。
|
||||
* 没有广告。
|
||||
|
||||
本应用是开源软件
|
||||
意味着您可以使用、学习、修改和分发本应用的源代码。
|
||||
本应用使用 Apache 2.0 协议发布。
|
||||
|
||||
@@ -1 +1 @@
|
||||
一个完全尊重你隐私的运动追踪器。
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -7,6 +7,7 @@ import junit.framework.TestCase;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -105,7 +106,7 @@ public class TrackSelectionTest extends TestCase {
|
||||
long oneDay = 24 * 60 * 60 * 1000;
|
||||
TrackSelection filterWrong1 = new TrackSelection().addDateRange(instant, null);
|
||||
TrackSelection filterWrong2 = new TrackSelection().addDateRange(null, instant);
|
||||
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plusMillis(oneDay));
|
||||
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plus(Duration.ofDays(1)));
|
||||
|
||||
// when
|
||||
SelectionData selectionWrong1 = filterWrong1.buildSelection();
|
||||
|
||||
+3
-3
@@ -28,14 +28,14 @@ public class KmlTrackExporterTest {
|
||||
@Test
|
||||
public void writeCloseSegment_only_write_sensordata_if_present() {
|
||||
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<coord/>\n" +
|
||||
"<when>1970-01-01T01:00:00+01:00</when>\n" +
|
||||
"<gx:coord/>\n" +
|
||||
"<coord/>\n" +
|
||||
"<ExtendedData>\n" +
|
||||
"<SchemaData schemaUrl=\"#schema\">\n" +
|
||||
"</SchemaData>\n" +
|
||||
"</ExtendedData>\n" +
|
||||
"</gx:Track>\n";
|
||||
"</Track>\n";
|
||||
|
||||
// given
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
|
||||
|
||||
+6
-5
@@ -61,7 +61,8 @@ 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.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
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.SensorDataSet;
|
||||
@@ -463,7 +464,7 @@ public class ExportImportTest {
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// then
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.csv_export);
|
||||
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
@@ -504,16 +505,16 @@ public class ExportImportTest {
|
||||
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
|
||||
sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate)));
|
||||
|
||||
SensorDataCycling.CyclingCadence cyclingCadence = Mockito.mock(SensorDataCycling.CyclingCadence.class);
|
||||
SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class);
|
||||
Mockito.when(cyclingCadence.hasValue()).thenReturn(true);
|
||||
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
|
||||
sensorDataSet.set(cyclingCadence);
|
||||
|
||||
if (distance != null && speed != null) {
|
||||
SensorDataCycling.DistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCycling.DistanceSpeed.Data.class);
|
||||
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
|
||||
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
|
||||
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
|
||||
SensorDataCycling.DistanceSpeed distanceSpeed = Mockito.mock(SensorDataCycling.DistanceSpeed.class);
|
||||
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
|
||||
Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
|
||||
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
|
||||
sensorDataSet.set(distanceSpeed);
|
||||
|
||||
+5
-5
@@ -69,7 +69,7 @@ public class GPXTrackImporterTest {
|
||||
public void gpx_with_pause_resume() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_gpx_pause_resume);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_gpx_pause_resume);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -123,7 +123,7 @@ public class GPXTrackImporterTest {
|
||||
public void gpx_without_speed() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_without_speed);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_without_speed);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -171,7 +171,7 @@ public class GPXTrackImporterTest {
|
||||
public void gpx_speed_no_namespace() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx11_with_speed_no_namespace);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx11_with_speed_no_namespace);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -221,8 +221,8 @@ public class GPXTrackImporterTest {
|
||||
public void importExportTest_timezone() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
|
||||
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
|
||||
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_timezone);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
|
||||
+5
-5
@@ -60,7 +60,7 @@ public class KMLTrackImporterTest {
|
||||
public void kml22_order_location_and_when() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_order_location_and_when);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_order_location_and_when);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -109,7 +109,7 @@ public class KMLTrackImporterTest {
|
||||
public void kml22_time_decreases() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_time_decreases);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_time_decreases);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -149,7 +149,7 @@ public class KMLTrackImporterTest {
|
||||
public void kml22_with_statistics_marker() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_kml22_statistics_marker);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_kml22_statistics_marker);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
@@ -223,7 +223,7 @@ public class KMLTrackImporterTest {
|
||||
public void kml_without_locations() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_without_locations);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_without_locations);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
@@ -234,7 +234,7 @@ public class KMLTrackImporterTest {
|
||||
public void kml_when_locations_different() throws IOException {
|
||||
// given
|
||||
XMLImporter importer = new XMLImporter(new KmlTrackImporter(context, trackImporter));
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.kml22_when_locations_different);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.kml22_when_locations_different);
|
||||
|
||||
// when
|
||||
importTrackId = importer.importFile(inputStream).get(0);
|
||||
|
||||
@@ -10,7 +10,8 @@ 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;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
public class BluetoothUtilsTest {
|
||||
@@ -18,7 +19,7 @@ public class BluetoothUtilsTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
@@ -31,7 +32,7 @@ public class BluetoothUtilsTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
@@ -43,11 +44,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertNull(sensor.getDistanceSpeed());
|
||||
@@ -56,11 +57,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
@@ -69,11 +70,11 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount());
|
||||
@@ -82,19 +83,34 @@ public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
|
||||
SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, power_w);
|
||||
assertEquals(40, powerCadence.getPower().getValue().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.getServiceUUID(), 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.getPower().getValue().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.getCadence().getCrankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.getCadence().getCrankRevolutionsTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseRunningSpeedAndCadence_with_distance() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.getServiceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
|
||||
|
||||
// when
|
||||
|
||||
+24
-24
@@ -21,8 +21,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_1() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -34,8 +34,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_2() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 8016);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -47,8 +47,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_sameCount() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -61,8 +61,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_sameTime() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 1024);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_rollOverTime() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 2, 0);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -89,8 +89,8 @@ public class SensorDataCyclingTest {
|
||||
@Deprecated
|
||||
public void compute_cadence_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_cadence_overflow() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
@@ -115,8 +115,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_speed() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184);
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016);
|
||||
|
||||
// when
|
||||
current.compute(previous, Distance.ofMM(2150));
|
||||
@@ -131,8 +131,8 @@ public class SensorDataCyclingTest {
|
||||
@Deprecated
|
||||
public void compute_speed_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
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));
|
||||
@@ -145,8 +145,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void compute_speed_overflow() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
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));
|
||||
@@ -158,8 +158,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void equals_speed_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress");
|
||||
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress");
|
||||
SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
@@ -172,8 +172,8 @@ public class SensorDataCyclingTest {
|
||||
@Test
|
||||
public void equals_cadence_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.CyclingCadence previous = new SensorDataCycling.CyclingCadence("sensorAddress");
|
||||
SensorDataCycling.CyclingCadence current = new SensorDataCycling.CyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress");
|
||||
SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
+8
-8
@@ -64,7 +64,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,7 +80,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Speed 20.0 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -96,7 +96,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,7 +112,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 19.99 kilometers. 1 hour. Speed 20.0 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 19.99 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +134,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,7 +188,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -210,7 +210,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -274,7 +274,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -99,7 +99,7 @@ public class DescriptionGeneratorTest {
|
||||
+ "Min elevation: -500 m (-1640 ft)<br>"
|
||||
+ "Elevation gain: 6000 m (19685 ft)<br>"
|
||||
+ "Elevation loss: 6000 m (19685 ft)<br>"
|
||||
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class DescriptionGeneratorTest {
|
||||
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
|
||||
+ "Elevation gain: 6000 m (19685 ft)<br>"
|
||||
+ "Elevation loss: 6000 m (19685 ft)<br>"
|
||||
+ "Recorded: " + StringUtils.formatDateTime(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
|
||||
+1
-2
@@ -21,7 +21,6 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
|
||||
@@ -180,7 +179,7 @@ public class IntervalStatisticsTest {
|
||||
|
||||
// then
|
||||
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
|
||||
assertEquals(trackStatistics.getTotalTime().toSeconds(), totalTime, 0.01);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
|
||||
if (totalGain != null) {
|
||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.dennisguse.opentracks"
|
||||
android:installLocation="auto">
|
||||
|
||||
<supports-screens
|
||||
@@ -59,6 +58,8 @@ limitations under the License.
|
||||
tools:targetApi="s" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".Startup"
|
||||
android:allowBackup="false"
|
||||
|
||||
@@ -84,8 +84,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
||||
/**
|
||||
* Called every time a track is deleted.
|
||||
*/
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||
if (trackDeleteServiceConnection != null) {
|
||||
trackDeleteServiceConnection.unbind(this);
|
||||
trackDeleteServiceConnection = null;
|
||||
onDeleteFinished();
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
@@ -70,7 +71,7 @@ import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.ui.util.ListItemUtils;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -158,8 +159,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Reset theme after splash
|
||||
setTheme(R.style.ThemeCustom);
|
||||
SplashScreen.installSplashScreen(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
@@ -263,13 +263,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
}
|
||||
|
||||
private void requestRequiredPermissions() {
|
||||
if (!PermissionUtils.hasGPSPermission(this)) {
|
||||
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
if (!PermissionUtils.hasBluetoothPermissions(this)) {
|
||||
PermissionUtils.requestBluetoothPermission(this, null, null);
|
||||
}
|
||||
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -232,9 +232,9 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
|
||||
super.onTrackDeleteStatus(deleteStatus);
|
||||
if (deleteStatus.isDeleted(trackId)) {
|
||||
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
|
||||
super.onTrackDeleteStatus(deletionFinishedStatus);
|
||||
if (deletionFinishedStatus.isDeleted(trackId)) {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.annotation.VisibleForTesting;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
@@ -41,7 +40,7 @@ public class ChartPoint {
|
||||
|
||||
if (smoothedSpeed != null) {
|
||||
speed = smoothedSpeed.to(unitSystem);
|
||||
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
|
||||
pace = smoothedSpeed.toPace(unitSystem).toSeconds() / 60d;
|
||||
}
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRate = (double) trackPoint.getHeartRate().getBPM();
|
||||
|
||||
@@ -187,7 +187,6 @@ public class ContentProviderUtils {
|
||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||
}
|
||||
|
||||
// Delete track last since it triggers a database vacuum call
|
||||
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
|
||||
}
|
||||
@@ -195,8 +194,6 @@ public class ContentProviderUtils {
|
||||
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
|
||||
// Delete track folder resources.
|
||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||
|
||||
// Delete track last since it triggers a database vacuum call
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -39,6 +40,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
||||
@@ -53,6 +55,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
|
||||
private static final String SQL_LIST_DELIMITER = ",";
|
||||
|
||||
private static final int TOTAL_DELETED_ROWS_VACUUM_THRESHOLD = 10000;
|
||||
|
||||
private final UriMatcher uriMatcher;
|
||||
|
||||
private SQLiteDatabase db;
|
||||
@@ -131,14 +135,12 @@ public class CustomContentProvider extends ContentProvider {
|
||||
@Override
|
||||
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
|
||||
String table;
|
||||
boolean shouldVacuum = false;
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS:
|
||||
table = TrackPointsColumns.TABLE_NAME;
|
||||
break;
|
||||
case TRACKS:
|
||||
table = TracksColumns.TABLE_NAME;
|
||||
shouldVacuum = true;
|
||||
break;
|
||||
case MARKERS:
|
||||
table = MarkerColumns.TABLE_NAME;
|
||||
@@ -147,23 +149,40 @@ public class CustomContentProvider extends ContentProvider {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Deleting table " + table);
|
||||
int count;
|
||||
Log.w(TAG, "Deleting from table " + table);
|
||||
int totalChangesBefore = getTotalChanges();
|
||||
int deletedRowsFromTable;
|
||||
try {
|
||||
db.beginTransaction();
|
||||
count = db.delete(table, where, selectionArgs);
|
||||
deletedRowsFromTable = db.delete(table, where, selectionArgs);
|
||||
Log.i(TAG, "Deleted " + deletedRowsFromTable + " rows of table " + table);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, false);
|
||||
|
||||
if (shouldVacuum) {
|
||||
// If a potentially large amount of data was deleted, reclaim its space.
|
||||
Log.i(TAG, "Vacuuming the database.");
|
||||
int totalChanges = getTotalChanges() - totalChangesBefore;
|
||||
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
|
||||
|
||||
PreferencesUtils.addTotalRowsDeleted(totalChanges);
|
||||
int totalRowsDeleted = PreferencesUtils.getTotalRowsDeleted();
|
||||
if (totalRowsDeleted > TOTAL_DELETED_ROWS_VACUUM_THRESHOLD) {
|
||||
Log.i(TAG, "TotalRowsDeleted " + totalRowsDeleted + ", starting to vacuum the database.");
|
||||
db.execSQL("VACUUM");
|
||||
PreferencesUtils.resetTotalRowsDeleted();
|
||||
}
|
||||
return count;
|
||||
|
||||
return deletedRowsFromTable;
|
||||
}
|
||||
|
||||
private int getTotalChanges() {
|
||||
int totalCount;
|
||||
try (Cursor cursor = db.rawQuery("SELECT total_changes()", null)) {
|
||||
cursor.moveToNext();
|
||||
totalCount = cursor.getInt(0);
|
||||
}
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,22 +2,35 @@ package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Cadence {
|
||||
|
||||
public static Cadence of(float value) {
|
||||
return new Cadence(value);
|
||||
public static Cadence of(float value, Duration duration) {
|
||||
if (duration.isZero()) {
|
||||
return zero();
|
||||
}
|
||||
|
||||
return new Cadence(value / (duration.toMillis() / (float) Duration.ofMinutes(1).toMillis()));
|
||||
}
|
||||
|
||||
private final float value;
|
||||
public static Cadence of(float value_rpm) {
|
||||
return new Cadence(value_rpm);
|
||||
}
|
||||
|
||||
public static Cadence zero() {
|
||||
return of(0.0f);
|
||||
}
|
||||
|
||||
private final float value_rpm;
|
||||
|
||||
private Cadence(float value) {
|
||||
this.value = value;
|
||||
this.value_rpm = value;
|
||||
}
|
||||
|
||||
public float getRPM() {
|
||||
return value;
|
||||
return value_rpm;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,19 +38,19 @@ public class Cadence {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Cadence cadence = (Cadence) o;
|
||||
return Float.compare(cadence.value, value) == 0;
|
||||
return Float.compare(cadence.value_rpm, value_rpm) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
return Objects.hash(value_rpm);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Cadence{" +
|
||||
"value=" + value + " rpm" +
|
||||
"value=" + value_rpm + " rpm" +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,15 @@ public class Distance {
|
||||
}
|
||||
|
||||
public static Distance ofMile(double distance_mile) {
|
||||
return of(distance_mile * UnitConversions.MI_TO_M);
|
||||
return of(distance_mile * MI_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofNauticalMile(double distance_mile) {
|
||||
return of(distance_mile * UnitConversions.NAUTICAL_MILE_TO_M);
|
||||
return of(distance_mile * NAUTICAL_MILE_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofKilometer(double distance_km) {
|
||||
return of(distance_km * UnitConversions.KM_TO_M);
|
||||
return of(distance_km * KM_TO_M);
|
||||
}
|
||||
|
||||
public static Distance ofMM(double distance_mm) {
|
||||
@@ -117,19 +117,19 @@ public class Distance {
|
||||
}
|
||||
|
||||
public double toKM() {
|
||||
return distance_m * UnitConversions.M_TO_KM;
|
||||
return distance_m * M_TO_KM;
|
||||
}
|
||||
|
||||
public double toFT() {
|
||||
return distance_m * UnitConversions.M_TO_FT;
|
||||
return distance_m * M_TO_FT;
|
||||
}
|
||||
|
||||
public double toMI() {
|
||||
return toKM() * UnitConversions.KM_TO_MI;
|
||||
return distance_m * M_TO_MI;
|
||||
}
|
||||
|
||||
public double toNauticalMiles() {
|
||||
return toKM() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||
return distance_m * M_TO_NAUTICAL_MILE;
|
||||
}
|
||||
|
||||
public double toKM_Miles(UnitSystem unitSystem) {
|
||||
@@ -177,4 +177,17 @@ public class Distance {
|
||||
"distance_m=" + distance_m +
|
||||
'}';
|
||||
}
|
||||
|
||||
// multiplication factors for conversion
|
||||
private static final double KM_TO_M = 1000.0;
|
||||
private static final double M_TO_KM = 1 / KM_TO_M;
|
||||
|
||||
public static final double MI_TO_M = 1609.344;
|
||||
public static final double M_TO_MI = 1 / MI_TO_M;
|
||||
|
||||
private static final double MI_TO_FT = 5280.0;
|
||||
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
|
||||
|
||||
private static final double NAUTICAL_MILE_TO_M = 1852.0;
|
||||
private static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@ public class DistanceFormatter {
|
||||
|
||||
private final int decimalCount;
|
||||
|
||||
private final double threshold;
|
||||
|
||||
private final UnitSystem unitSystem;
|
||||
|
||||
private DistanceFormatter(Resources resources, int decimalCount, UnitSystem unitSystem) {
|
||||
private DistanceFormatter(Resources resources, int decimalCount, double threshold, UnitSystem unitSystem) {
|
||||
this.resources = resources;
|
||||
this.decimalCount = decimalCount;
|
||||
this.threshold = threshold;
|
||||
this.unitSystem = unitSystem;
|
||||
assert unitSystem != null;
|
||||
}
|
||||
@@ -56,19 +59,19 @@ public class DistanceFormatter {
|
||||
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (distance.greaterThan(Distance.of(500))) {
|
||||
if (distance.greaterThan(Distance.ofKilometer(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
|
||||
}
|
||||
case IMPERIAL:
|
||||
if (distance.greaterThan(Distance.ofMile(0.5))) {
|
||||
if (distance.greaterThan(Distance.ofMile(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
case NAUTICAL_IMPERIAL:
|
||||
if (distance.greaterThan(Distance.ofNauticalMile(0.5))) {
|
||||
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
@@ -88,8 +91,11 @@ public class DistanceFormatter {
|
||||
|
||||
private UnitSystem unitSystem;
|
||||
|
||||
private double threshold;
|
||||
|
||||
public Builder() {
|
||||
decimalCount = 2;
|
||||
threshold = 0.5;
|
||||
}
|
||||
|
||||
public Builder setDecimalCount(int decimalCount) {
|
||||
@@ -102,8 +108,13 @@ public class DistanceFormatter {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setThreshold(double threshold) {
|
||||
this.threshold = threshold;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Resources resource) {
|
||||
return new DistanceFormatter(resource, decimalCount, unitSystem);
|
||||
return new DistanceFormatter(resource, decimalCount, threshold, unitSystem);
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Context context) {
|
||||
|
||||
@@ -15,7 +15,7 @@ public class Speed {
|
||||
return zero();
|
||||
}
|
||||
|
||||
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
|
||||
return new Speed(distance.toM() / (duration.toMillis() / (double) Duration.ofSeconds(1).toMillis()));
|
||||
}
|
||||
|
||||
public static Speed of(double speed_mps) {
|
||||
@@ -85,16 +85,23 @@ public class Speed {
|
||||
return speed_mps;
|
||||
}
|
||||
|
||||
/**
|
||||
* We interpret {@link Speed} here as a {@link Distance} over 1h.
|
||||
*/
|
||||
private Distance toH() {
|
||||
return Distance.of(speed_mps * Duration.ofHours(1).toSeconds());
|
||||
}
|
||||
|
||||
public double toKMH() {
|
||||
return speed_mps * UnitConversions.MPS_TO_KMH;
|
||||
return toH().toKM();
|
||||
}
|
||||
|
||||
public double toMPH() {
|
||||
return toKMH() * UnitConversions.KM_TO_MI;
|
||||
return toH().toMI();
|
||||
}
|
||||
|
||||
public double toKnots() {
|
||||
return toKMH() * UnitConversions.KM_TO_NAUTICAL_MILE;
|
||||
return toH().toNauticalMiles();
|
||||
}
|
||||
|
||||
public Duration toPace(UnitSystem unitSystem) {
|
||||
@@ -102,20 +109,7 @@ public class Speed {
|
||||
return Duration.ofSeconds(0);
|
||||
}
|
||||
|
||||
double distance = speed_mps;
|
||||
switch (unitSystem) { //TODO Can we use to(UnitSystem)?
|
||||
case METRIC:
|
||||
distance *= UnitConversions.M_TO_KM;
|
||||
break;
|
||||
case IMPERIAL:
|
||||
distance *= UnitConversions.M_TO_MI;
|
||||
break;
|
||||
case NAUTICAL_IMPERIAL:
|
||||
distance *= UnitConversions.M_TO_NAUTICAL_MILE;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
double distance = Distance.of(speed_mps).toKM_Miles(unitSystem);
|
||||
|
||||
return Duration.ofSeconds(Math.round(1 / distance));
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
/**
|
||||
* Unit conversion constants.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class UnitConversions {
|
||||
|
||||
// Time //TODO Use Duration
|
||||
// multiplication factor to convert seconds to milliseconds
|
||||
@Deprecated
|
||||
public static final long S_TO_MS = 1000;
|
||||
|
||||
// multiplication factor to convert milliseconds to seconds
|
||||
@Deprecated
|
||||
public static final double MS_TO_S = 1d / S_TO_MS;
|
||||
// multiplication factor to convert minutes to seconds
|
||||
@Deprecated
|
||||
public static final double MIN_TO_S = 60.0;
|
||||
// multiplication factor to convert seconds to minutes
|
||||
@Deprecated
|
||||
public static final double S_TO_MIN = 1 / MIN_TO_S;
|
||||
// multiplication factor to convert minutes to hours
|
||||
@Deprecated
|
||||
public static final double MIN_TO_HR = 1 / 60.0;
|
||||
|
||||
// multiplication factor to convert kilometers to miles
|
||||
public static final double KM_TO_MI = 0.621371192;
|
||||
|
||||
// Distance //TODO Make private to Distance class!
|
||||
// multiplication factor to convert miles to feet
|
||||
private static final double MI_TO_FT = 5280.0;
|
||||
// multiplication factor to covert kilometers to meters
|
||||
public static final double KM_TO_M = 1000.0;
|
||||
// multiplication factor to convert meters to kilometers
|
||||
public static final double M_TO_KM = 1 / KM_TO_M;
|
||||
// multiplication factor to convert meters to miles
|
||||
public static final double M_TO_MI = M_TO_KM * KM_TO_MI;
|
||||
// multiplication factor to convert meters to feet
|
||||
public static final double M_TO_FT = M_TO_MI * MI_TO_FT;
|
||||
public static final double NAUTICAL_MILE_TO_M = 1852.0;
|
||||
public static final double M_TO_NAUTICAL_MILE = 1 / NAUTICAL_MILE_TO_M;
|
||||
public static final double KM_TO_NAUTICAL_MILE = 1000 * M_TO_NAUTICAL_MILE;
|
||||
// multiplication factor to convert meters per second to kilometers per hour
|
||||
public static final double MPS_TO_KMH = M_TO_KM / (S_TO_MIN * MIN_TO_HR);
|
||||
// multiplication factor to convert miles to km
|
||||
public static final double MI_TO_KM = 1 / KM_TO_MI;
|
||||
// multiplication factor to convert miles to m
|
||||
public static final double MI_TO_M = MI_TO_KM * KM_TO_M;
|
||||
|
||||
private UnitConversions() {
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
private void loadTrackDescription(@NonNull Track track) {
|
||||
viewBinding.statsNameValue.setText(track.getName());
|
||||
viewBinding.statsDescriptionValue.setText(track.getDescription());
|
||||
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTime(track.getStartTime()));
|
||||
viewBinding.statsStartDatetimeValue.setText(StringUtils.formatDateTimeWithOffsetIfDifferent(track.getStartTime()));
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
|
||||
@@ -44,7 +44,8 @@ import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML.
|
||||
* Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML version 2.3.
|
||||
* https://docs.opengeospatial.org/is/12-007r2/12-007r2.html
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
@@ -58,6 +59,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private static final String TRACK_STYLE = "track";
|
||||
private static final String SCHEMA_ID = "schema";
|
||||
|
||||
public static final String EXTENDED_DATA_TYPE_CATEGORY = "type";
|
||||
|
||||
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
|
||||
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
|
||||
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
|
||||
@@ -224,11 +227,13 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private void writeHeader(Track[] tracks) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
|
||||
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
|
||||
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.3\"");
|
||||
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
|
||||
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">");
|
||||
//TODO ADD xsi:schemaLocation here!
|
||||
//TODO ADD xsi:schemaLocation for atom
|
||||
printWriter.println("xsi:schemaLocation=" +
|
||||
"\"http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd"
|
||||
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
|
||||
|
||||
printWriter.println("<Document>");
|
||||
printWriter.println("<open>1</open>");
|
||||
@@ -309,16 +314,16 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
|
||||
writeCategory(track.getCategory());
|
||||
printWriter.println("<gx:MultiTrack>");
|
||||
printWriter.println("<MultiTrack>");
|
||||
printWriter.println("<altitudeMode>absolute</altitudeMode>");
|
||||
printWriter.println("<gx:interpolate>1</gx:interpolate>");
|
||||
printWriter.println("<interpolate>1</interpolate>");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeEndTrack() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gx:MultiTrack>");
|
||||
printWriter.println("</MultiTrack>");
|
||||
printWriter.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
@@ -326,7 +331,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
@VisibleForTesting
|
||||
void writeOpenSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<gx:Track>");
|
||||
printWriter.println("<Track>");
|
||||
speedList.clear();
|
||||
distanceList.clear();
|
||||
powerList.clear();
|
||||
@@ -373,7 +378,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
}
|
||||
printWriter.println("</SchemaData>");
|
||||
printWriter.println("</ExtendedData>");
|
||||
printWriter.println("</gx:Track>");
|
||||
printWriter.println("</Track>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,9 +388,9 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
|
||||
printWriter.println("<coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</coord>");
|
||||
} else {
|
||||
printWriter.println("<gx:coord/>");
|
||||
printWriter.println("<coord/>");
|
||||
}
|
||||
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
|
||||
|
||||
@@ -408,16 +413,16 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
* @param name the name of the simple array data
|
||||
*/
|
||||
private void writeSimpleArrayData(List<Float> list, String name) {
|
||||
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
|
||||
printWriter.println("<SimpleArrayData name=\"" + name + "\">");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Float value = list.get(i);
|
||||
if (value == null) {
|
||||
printWriter.println("<gx:value />");
|
||||
printWriter.println("<value />");
|
||||
} else {
|
||||
printWriter.println("<gx:value>" + SENSOR_DATA_FORMAT.format(value) + "</gx:value>");
|
||||
printWriter.println("<value>" + SENSOR_DATA_FORMAT.format(value) + "</value>");
|
||||
}
|
||||
}
|
||||
printWriter.println("</gx:SimpleArrayData>");
|
||||
printWriter.println("</SimpleArrayData>");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,7 +526,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
return;
|
||||
}
|
||||
printWriter.println("<ExtendedData>");
|
||||
printWriter.println("<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
|
||||
printWriter.println("<Data name=\"" + EXTENDED_DATA_TYPE_CATEGORY + "\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
|
||||
printWriter.println("</ExtendedData>");
|
||||
}
|
||||
|
||||
@@ -554,8 +559,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
* @param extendedDataType the extended data display name
|
||||
*/
|
||||
private void writeSimpleArrayStyle(String name, String extendedDataType) {
|
||||
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
|
||||
printWriter.println("<SimpleArrayField name=\"" + name + "\" type=\"float\">");
|
||||
printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
|
||||
printWriter.println("</gx:SimpleArrayField>");
|
||||
printWriter.println("</SimpleArrayField>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -48,7 +47,14 @@ public class ImportViewModel extends AndroidViewModel implements ImportServiceRe
|
||||
}
|
||||
|
||||
private void loadData(List<DocumentFile> documentFiles) {
|
||||
List<DocumentFile> fileList = documentFiles.stream().map(FileUtils::getFiles).flatMap(Collection::stream).collect(Collectors.toList());
|
||||
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<>();
|
||||
nestedFileList.forEach(fileList::addAll);
|
||||
|
||||
summary.totalCount = fileList.size();
|
||||
filesToImport.addAll(fileList);
|
||||
importNextFile();
|
||||
|
||||
@@ -41,7 +41,7 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
* Imports a KML file; preferred version: KML2.3, but also supports KML2.2.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@@ -54,18 +54,31 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private static final String TAG_COORDINATES = "coordinates";
|
||||
private static final String TAG_DESCRIPTION = "description";
|
||||
private static final String TAG_ICON = "icon";
|
||||
private static final String TAG_GX_COORD = "gx:coord";
|
||||
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
|
||||
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
|
||||
private static final String TAG_GX_TRACK = "gx:Track";
|
||||
private static final String TAG_GX_VALUE = "gx:value";
|
||||
|
||||
private static final String TAG_COORD = "coord";
|
||||
private static final String TAG_KML22_COORD = "gx:coord";
|
||||
|
||||
private static final String TAG_MULTI_TRACK = "MultiTrack";
|
||||
private static final String TAG_KML22_MULTI_TRACK = "gx:MultiTrack";
|
||||
|
||||
private static final String TAG_DATA_CATEGORY = "Data"; //used for Track.category
|
||||
|
||||
private static final String TAG_SIMPLE_ARRAY_DATA = "SimpleArrayData";
|
||||
private static final String TAG_KML22_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
|
||||
|
||||
private static final String TAG_TRACK = "Track";
|
||||
private static final String TAG_KML22_TRACK = "gx:Track";
|
||||
|
||||
private static final String TAG_VALUE = "value";
|
||||
private static final String TAG_KML22_VALUE = "gx:value";
|
||||
|
||||
private static final String TAG_HREF = "href";
|
||||
private static final String TAG_KML = "kml";
|
||||
private static final String TAG_NAME = "name";
|
||||
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
|
||||
private static final String TAG_PLACEMARK = "Placemark";
|
||||
private static final String TAG_STYLE_URL = "styleUrl";
|
||||
private static final String TAG_VALUE = "value";
|
||||
// private static final String TAG_VALUE = "value"; TODO
|
||||
private static final String TAG_WHEN = "when";
|
||||
private static final String TAG_UUID = "opentracks:trackid";
|
||||
|
||||
@@ -81,7 +94,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private final ArrayList<Instant> whenList = new ArrayList<>();
|
||||
private final ArrayList<Location> locationList = new ArrayList<>();
|
||||
|
||||
private String extendedDataType;
|
||||
private String dataType;
|
||||
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
|
||||
@@ -128,17 +141,21 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description.
|
||||
onMarkerStart();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
case TAG_MULTI_TRACK:
|
||||
case TAG_KML22_MULTI_TRACK:
|
||||
trackImporter.newTrack();
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
case TAG_TRACK:
|
||||
case TAG_KML22_TRACK:
|
||||
if (trackImporter == null) {
|
||||
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
||||
throw new SAXException("Missing " + TAG_MULTI_TRACK);
|
||||
}
|
||||
onTrackSegmentStart();
|
||||
break;
|
||||
case TAG_GX_SIMPLE_ARRAY_DATA:
|
||||
onExtendedDataStart(attributes);
|
||||
case TAG_DATA_CATEGORY:
|
||||
case TAG_SIMPLE_ARRAY_DATA:
|
||||
case TAG_KML22_SIMPLE_ARRAY_DATA:
|
||||
dataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -162,18 +179,28 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
case TAG_COORDINATES:
|
||||
onMarkerLocationEnd();
|
||||
break;
|
||||
case TAG_GX_MULTI_TRACK:
|
||||
case TAG_MULTI_TRACK:
|
||||
case TAG_KML22_MULTI_TRACK:
|
||||
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
|
||||
zoneOffset = null;
|
||||
break;
|
||||
case TAG_GX_TRACK:
|
||||
case TAG_TRACK:
|
||||
case TAG_KML22_TRACK:
|
||||
onTrackSegmentEnd();
|
||||
break;
|
||||
case TAG_GX_COORD:
|
||||
case TAG_COORD:
|
||||
case TAG_KML22_COORD:
|
||||
onCoordEnded();
|
||||
break;
|
||||
case TAG_GX_VALUE:
|
||||
onExtendedDataValueEnd();
|
||||
case TAG_VALUE:
|
||||
case TAG_KML22_VALUE:
|
||||
if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) {
|
||||
if (content != null) {
|
||||
category = content.trim();
|
||||
}
|
||||
} else {
|
||||
onExtendedDataValueEnd();
|
||||
}
|
||||
break;
|
||||
case TAG_NAME:
|
||||
if (content != null) {
|
||||
@@ -195,11 +222,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
icon = content.trim();
|
||||
}
|
||||
break;
|
||||
case TAG_VALUE:
|
||||
if (content != null) {
|
||||
category = content.trim();
|
||||
}
|
||||
break;
|
||||
case TAG_WHEN:
|
||||
if (content != null) {
|
||||
try {
|
||||
@@ -401,11 +423,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
private void onExtendedDataStart(Attributes attributes) {
|
||||
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
private void onExtendedDataValueEnd() throws SAXException {
|
||||
Float value = null;
|
||||
if (content != null) {
|
||||
@@ -414,11 +431,11 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
try {
|
||||
value = Float.parseFloat(content);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
|
||||
throw new SAXException(createErrorMessage("Unable to parse value:" + content), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (extendedDataType) {
|
||||
switch (dataType) {
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
|
||||
sensorSpeedList.add(value);
|
||||
break;
|
||||
@@ -447,7 +464,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
accuracyVertical.add(value);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
|
||||
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.Marker;
|
||||
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.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
@@ -290,4 +291,5 @@ public class TrackImporter {
|
||||
public void cleanImport() {
|
||||
contentProviderUtils.deleteTracks(context, trackIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
@@ -30,11 +31,14 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
splashScreen.setKeepOnScreenCondition(() -> true );
|
||||
|
||||
if (PreferencesUtils.isPublicAPIenabled()) {
|
||||
Log.i(TAG, "Received and trying to execute requested action.");
|
||||
new TrackRecordingServiceConnection(serviceConnectedCallback)
|
||||
.startAndBind(this, true);
|
||||
.startAndBind(this, isStartServiceForeground());
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
|
||||
Log.w(TAG, "Public API is disabled; ignoring request.");
|
||||
@@ -42,6 +46,10 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isStartServiceForeground() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract void execute(TrackRecordingService service);
|
||||
|
||||
protected abstract boolean isPostExecuteStopService();
|
||||
|
||||
@@ -15,6 +15,7 @@ public class StartRecording extends AbstractAPIActivity {
|
||||
|
||||
public static final String EXTRA_TRACK_NAME = "TRACK_NAME";
|
||||
public static final String EXTRA_TRACK_CATEGORY = "TRACK_CATEGORY";
|
||||
public static final String EXTRA_TRACK_ICON = "TRACK_ICON";
|
||||
public static final String EXTRA_TRACK_DESCRIPTION = "TRACK_DESCRIPTION";
|
||||
|
||||
public static final String EXTRA_STATS_TARGET_PACKAGE = "STATS_TARGET_PACKAGE";
|
||||
@@ -40,8 +41,11 @@ public class StartRecording extends AbstractAPIActivity {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackUtils.updateTrack(this, track, bundle.getString(EXTRA_TRACK_NAME, null),
|
||||
bundle.getString(EXTRA_TRACK_CATEGORY, null), bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
|
||||
TrackUtils.updateTrack(this, track,
|
||||
bundle.getString(EXTRA_TRACK_NAME, null),
|
||||
bundle.getString(EXTRA_TRACK_CATEGORY, null),
|
||||
bundle.getString(EXTRA_TRACK_ICON, null),
|
||||
bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
|
||||
contentProviderUtils);
|
||||
}
|
||||
|
||||
@@ -57,4 +61,9 @@ public class StartRecording extends AbstractAPIActivity {
|
||||
protected boolean isPostExecuteStopService() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isStartServiceForeground() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,11 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
/**
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
@@ -51,8 +46,7 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
|
||||
private final SensorDataObserver observer;
|
||||
|
||||
private final UUID serviceUUUID;
|
||||
private final UUID measurementUUID;
|
||||
private final List<ServiceMeasurementUUID> serviceMeasurementUUIDs;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@@ -81,15 +75,24 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService service = gatt.getService(serviceUUUID);
|
||||
if (service == null) {
|
||||
Log.e(TAG, "Could not get service for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID);
|
||||
BluetoothGattService gattService = null;
|
||||
ServiceMeasurementUUID serviceMeasurement = null;
|
||||
for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) {
|
||||
gattService = gatt.getService(s.getServiceUUID());
|
||||
if (gattService != null) {
|
||||
serviceMeasurement = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (gattService == null) {
|
||||
Log.e(TAG, "Could not get gattService for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement);
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic characteristic = service.getCharacteristic(measurementUUID);
|
||||
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.getMeasurementUUID());
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID + " characteristicUUID=" + measurementUUID);
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement.getServiceUUID() + " characteristicUUID=" + serviceMeasurement.getMeasurementUUID());
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
@@ -108,8 +111,16 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
|
||||
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), 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.getServiceUUID().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) {
|
||||
@@ -122,15 +133,20 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
}
|
||||
};
|
||||
|
||||
BluetoothConnectionManager(UUID serviceUUUID, UUID measurementUUID, SensorDataObserver observer) {
|
||||
this.serviceUUUID = serviceUUUID;
|
||||
this.measurementUUID = measurementUUID;
|
||||
BluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = List.of(serviceUUUID);
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
BluetoothConnectionManager(List<ServiceMeasurementUUID> serviceUUUID, SensorDataObserver observer) {
|
||||
this.serviceMeasurementUUIDs = serviceUUUID;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
|
||||
if (bluetoothGatt != null) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Connecting to: " + device);
|
||||
@@ -172,114 +188,7 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
/**
|
||||
* @return null if data could not be parsed.
|
||||
*/
|
||||
protected abstract SensorData<DataType> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
|
||||
|
||||
public static class HeartRateConnectionManager extends BluetoothConnectionManager<HeartRate> {
|
||||
|
||||
HeartRateConnectionManager(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataHeartRate createEmptySensorData(String address) {
|
||||
return new SensorDataHeartRate(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
return heartRate != null ? new SensorDataHeartRate(address, sensorName, HeartRate.of(heartRate)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CyclingCadence extends BluetoothConnectionManager<Cadence> {
|
||||
|
||||
CyclingCadence(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCycling.CyclingCadence createEmptySensorData(String address) {
|
||||
return new SensorDataCycling.CyclingCadence(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCycling.CyclingCadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cadenceAndSpeed.getCadence() != null) {
|
||||
return cadenceAndSpeed.getCadence();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCycling.DistanceSpeed.Data> {
|
||||
|
||||
CyclingDistanceSpeed(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCycling.DistanceSpeed createEmptySensorData(String address) {
|
||||
return new SensorDataCycling.DistanceSpeed(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cadenceAndSpeed.getDistanceSpeed() != null) {
|
||||
return cadenceAndSpeed.getDistanceSpeed();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CyclingPower extends BluetoothConnectionManager<Power> {
|
||||
|
||||
CyclingPower(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingPower createEmptySensorData(String address) {
|
||||
return new SensorDataCyclingPower(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCyclingPower parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Integer cyclingPower = BluetoothUtils.parseCyclingPower(characteristic);
|
||||
|
||||
return cyclingPower != null ? new SensorDataCyclingPower(address, sensorName, Power.of(cyclingPower)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> {
|
||||
|
||||
RunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID, BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataRunning createEmptySensorData(String address) {
|
||||
return new SensorDataRunning(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataRunning parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
|
||||
}
|
||||
}
|
||||
protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic);
|
||||
|
||||
interface SensorDataObserver {
|
||||
|
||||
|
||||
@@ -31,11 +31,12 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
/**
|
||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||
@@ -64,11 +65,11 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
||||
|
||||
private Distance preferenceWheelCircumference;
|
||||
|
||||
private final BluetoothConnectionManager.HeartRateConnectionManager heartRate = new BluetoothConnectionManager.HeartRateConnectionManager(this);
|
||||
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
||||
private final BluetoothConnectionManager.CyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingDistanceSpeed(this);
|
||||
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
|
||||
private final BluetoothConnectionManager.RunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionManager.RunningSpeedAndCadence(this);
|
||||
private final BluetoothRemoteSensorManagerHeartRate heartRate = new BluetoothRemoteSensorManagerHeartRate(this);
|
||||
private final BluetoothRemoteSensorManagerCyclingCadence cyclingCadence = new BluetoothRemoteSensorManagerCyclingCadence(this);
|
||||
private final BluetoothRemoteSensorManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothRemoteSensorManagerCyclingDistanceSpeed(this);
|
||||
private final BluetoothRemoteSensorManagerCyclingPower cyclingPower = new BluetoothRemoteSensorManagerCyclingPower(this);
|
||||
private final BluetoothRemoteSensorManagerRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothRemoteSensorManagerRunningSpeedAndCadence(this);
|
||||
|
||||
private final SensorDataSet sensorDataSet = new SensorDataSet();
|
||||
|
||||
@@ -162,7 +163,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
||||
} else {
|
||||
connectionManager.disconnect();
|
||||
}
|
||||
if (!PermissionUtils.hasBluetoothPermissions(context)) {
|
||||
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
|
||||
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
|
||||
}
|
||||
|
||||
@@ -186,24 +187,24 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
||||
|
||||
@Override
|
||||
public synchronized void onChanged(SensorData<?> sensorData) {
|
||||
if (sensorData instanceof SensorDataCycling.CyclingCadence) {
|
||||
SensorDataCycling.CyclingCadence previous = sensorDataSet.getCyclingCadence();
|
||||
if (sensorData instanceof SensorDataCyclingCadence) {
|
||||
SensorDataCyclingCadence previous = sensorDataSet.getCyclingCadence();
|
||||
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
|
||||
|
||||
if (sensorData.equals(previous)) {
|
||||
Log.d(TAG, "onChanged: cadence data repeated.");
|
||||
return;
|
||||
}
|
||||
((SensorDataCycling.CyclingCadence) sensorData).compute(previous);
|
||||
((SensorDataCyclingCadence) sensorData).compute(previous);
|
||||
}
|
||||
if (sensorData instanceof SensorDataCycling.DistanceSpeed) {
|
||||
SensorDataCycling.DistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
|
||||
if (sensorData instanceof SensorDataCyclingDistanceSpeed) {
|
||||
SensorDataCyclingDistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
|
||||
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
|
||||
if (sensorData.equals(previous)) {
|
||||
Log.d(TAG, "onChanged: cycling speed data repeated.");
|
||||
return;
|
||||
}
|
||||
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
|
||||
((SensorDataCyclingDistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
|
||||
}
|
||||
if (sensorData instanceof SensorDataRunning) {
|
||||
SensorDataRunning previous = sensorDataSet.getRunningDistanceSpeedCadence();
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
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 BluetoothRemoteSensorManagerCyclingCadence extends BluetoothConnectionManager<Cadence> {
|
||||
|
||||
private static final String TAG = BluetoothRemoteSensorManagerCyclingCadence.class.getSimpleName();
|
||||
|
||||
BluetoothRemoteSensorManagerCyclingCadence(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.getCadence();
|
||||
}
|
||||
} 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
@@ -0,0 +1,32 @@
|
||||
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 BluetoothRemoteSensorManagerCyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCyclingDistanceSpeed.Data> {
|
||||
|
||||
BluetoothRemoteSensorManagerCyclingDistanceSpeed(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
@@ -0,0 +1,27 @@
|
||||
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 BluetoothRemoteSensorManagerCyclingPower extends BluetoothConnectionManager<Power> {
|
||||
|
||||
BluetoothRemoteSensorManagerCyclingPower(@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.getPower() : null;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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 BluetoothRemoteSensorManagerHeartRate extends BluetoothConnectionManager<HeartRate> {
|
||||
|
||||
BluetoothRemoteSensorManagerHeartRate(@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) {
|
||||
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
return heartRate != null ? new SensorDataHeartRate(address, sensorName, HeartRate.of(heartRate)) : null;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
|
||||
|
||||
public class BluetoothRemoteSensorManagerRunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> {
|
||||
|
||||
BluetoothRemoteSensorManagerRunningSpeedAndCadence(@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);
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,12 @@ import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -43,23 +47,40 @@ public class BluetoothUtils {
|
||||
|
||||
public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
|
||||
|
||||
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
|
||||
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A3700001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final List<UUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
|
||||
BluetoothUtils.HEART_RATE_SERVICE_UUID,
|
||||
// Used for device discovery in preferences
|
||||
public static final List<ServiceMeasurementUUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
|
||||
HEARTRATE,
|
||||
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb") //Miband3
|
||||
));
|
||||
new ServiceMeasurementUUID(
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3
|
||||
HEARTRATE.getMeasurementUUID()
|
||||
))
|
||||
);
|
||||
|
||||
public static final UUID CYCLING_POWER_UUID = new UUID(0x181800001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID CYCLING_POWER_MEASUREMENT_CHAR_UUID = new UUID(0x2A6300001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
|
||||
new UUID(0x181800001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181600001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A5B00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_UUID = new UUID(0x181400001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID = new UUID(0x2A5300001000L, 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();
|
||||
|
||||
@@ -98,17 +119,49 @@ public class BluetoothUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Integer parseCyclingPower(BluetoothGattCharacteristic characteristic) {
|
||||
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 < 4) {
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
|
||||
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 SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
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) {
|
||||
@@ -120,25 +173,25 @@ public class BluetoothUtils {
|
||||
boolean hasCrank = (flags & 0x02) > 0;
|
||||
|
||||
int index = 1;
|
||||
SensorDataCycling.DistanceSpeed speed = null;
|
||||
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 SensorDataCycling.DistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
|
||||
speed = new SensorDataCyclingDistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
SensorDataCycling.CyclingCadence cadence = null;
|
||||
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 SensorDataCycling.CyclingCadence(address, sensorName, crankCount, crankTime);
|
||||
cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
|
||||
return new SensorDataCyclingCadenceAndDistanceSpeed(address, sensorName, cadence, speed);
|
||||
}
|
||||
|
||||
public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ServiceMeasurementUUID {
|
||||
|
||||
private final UUID serviceUUID;
|
||||
private final UUID measurementUUID;
|
||||
|
||||
public ServiceMeasurementUUID(UUID serviceUUID, UUID measurementUUID) {
|
||||
this.serviceUUID = serviceUUID;
|
||||
this.measurementUUID = measurementUUID;
|
||||
}
|
||||
|
||||
public UUID getServiceUUID() {
|
||||
return serviceUUID;
|
||||
}
|
||||
|
||||
public UUID getMeasurementUUID() {
|
||||
return measurementUUID;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServiceMeasurementUUID{" +
|
||||
"serviceUUID=" + serviceUUID +
|
||||
", measurementUUID=" + measurementUUID +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ServiceMeasurementUUID that = (ServiceMeasurementUUID) o;
|
||||
return Objects.equals(serviceUUID, that.serviceUUID) && Objects.equals(measurementUUID, that.measurementUUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(serviceUUID, measurementUUID);
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
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.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
|
||||
/**
|
||||
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors.
|
||||
*/
|
||||
public final class SensorDataCycling {
|
||||
|
||||
private static final String TAG = SensorDataCycling.class.getSimpleName();
|
||||
|
||||
private SensorDataCycling() {
|
||||
}
|
||||
|
||||
public static class CyclingCadence extends SensorData<Cadence> {
|
||||
|
||||
private final Long crankRevolutionsCount; // UINT32
|
||||
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||
|
||||
public CyclingCadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.crankRevolutionsCount = null;
|
||||
this.crankRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public CyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.crankRevolutionsCount = crankRevolutionsCount;
|
||||
this.crankRevolutionsTime = crankRevolutionsTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for Wahoo CADENCE: provides speed instead of cadence
|
||||
*/
|
||||
public CyclingCadence(@NonNull DistanceSpeed speed) {
|
||||
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
|
||||
}
|
||||
|
||||
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(CyclingCadence previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
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);
|
||||
float cadence_ms = crankDiff / timeDiff_ms;
|
||||
value = Cadence.of((float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof CyclingCadence)) return false;
|
||||
|
||||
CyclingCadence comp = (CyclingCadence) obj;
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class DistanceSpeed extends SensorData<DistanceSpeed.Data> {
|
||||
|
||||
private final Long wheelRevolutionsCount; // UINT32
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
|
||||
public DistanceSpeed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.wheelRevolutionsCount = null;
|
||||
this.wheelRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public DistanceSpeed(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(DistanceSpeed previous, Distance wheelCircumference) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
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 DistanceSpeed)) return false;
|
||||
|
||||
DistanceSpeed comp = (DistanceSpeed) obj;
|
||||
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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CadenceAndSpeed extends SensorData<Pair<CyclingCadence, DistanceSpeed>> {
|
||||
|
||||
public CadenceAndSpeed(String sensorAddress, String sensorName, @Nullable CyclingCadence cadence, @Nullable DistanceSpeed distanceSpeed) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.value = new Pair<>(cadence, distanceSpeed);
|
||||
}
|
||||
|
||||
public CyclingCadence getCadence() {
|
||||
return this.value != null ? this.value.first : null;
|
||||
}
|
||||
|
||||
public DistanceSpeed getDistanceSpeed() {
|
||||
return this.value != null ? this.value.second : null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Pair<CyclingCadence, DistanceSpeed> getNoneValue() {
|
||||
return new Pair<>(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
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)) return false;
|
||||
|
||||
SensorDataCyclingCadence comp = (SensorDataCyclingCadence) obj;
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
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)) return false;
|
||||
|
||||
SensorDataCyclingDistanceSpeed comp = (SensorDataCyclingDistanceSpeed) obj;
|
||||
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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-4
@@ -1,6 +1,7 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
|
||||
@@ -17,13 +18,40 @@ public class SensorDataCyclingPower extends SensorData<Power> {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " power=" + value;
|
||||
protected Power getNoneValue() {
|
||||
return Power.of(0f);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Power getNoneValue() {
|
||||
return Power.of(0f);
|
||||
public String toString() {
|
||||
return super.toString() + " data=" + value;
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
private final SensorDataCyclingPower power;
|
||||
private final SensorDataCyclingCadence cadence;
|
||||
|
||||
public Data(SensorDataCyclingPower power, @Nullable SensorDataCyclingCadence cadence) {
|
||||
this.power = power;
|
||||
this.cadence = cadence;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public SensorDataCyclingCadence getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Data{" +
|
||||
"power=" + power +
|
||||
", cadence=" + cadence +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ public final class SensorDataSet {
|
||||
|
||||
private SensorDataHeartRate heartRate;
|
||||
|
||||
private SensorDataCycling.CyclingCadence cyclingCadence;
|
||||
private SensorDataCyclingCadence cyclingCadence;
|
||||
|
||||
private SensorDataCycling.DistanceSpeed cyclingDistanceSpeed;
|
||||
private SensorDataCyclingDistanceSpeed cyclingDistanceSpeed;
|
||||
|
||||
private SensorDataCyclingPower cyclingPower;
|
||||
|
||||
@@ -64,11 +64,11 @@ public final class SensorDataSet {
|
||||
return null;
|
||||
}
|
||||
|
||||
public SensorDataCycling.CyclingCadence getCyclingCadence() {
|
||||
public SensorDataCyclingCadence getCyclingCadence() {
|
||||
return cyclingCadence;
|
||||
}
|
||||
|
||||
public SensorDataCycling.DistanceSpeed getCyclingDistanceSpeed() {
|
||||
public SensorDataCyclingDistanceSpeed getCyclingDistanceSpeed() {
|
||||
return cyclingDistanceSpeed;
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@ public final class SensorDataSet {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCycling.CyclingCadence) {
|
||||
this.cyclingCadence = (SensorDataCycling.CyclingCadence) data;
|
||||
if (type instanceof SensorDataCyclingCadence) {
|
||||
this.cyclingCadence = (SensorDataCyclingCadence) data;
|
||||
return;
|
||||
}
|
||||
if (type instanceof SensorDataCycling.DistanceSpeed) {
|
||||
this.cyclingDistanceSpeed = (SensorDataCycling.DistanceSpeed) data;
|
||||
if (type instanceof SensorDataCyclingDistanceSpeed) {
|
||||
this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) data;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class TrackDeleteService extends Service {
|
||||
|
||||
@@ -31,7 +33,7 @@ public class TrackDeleteService extends Service {
|
||||
|
||||
private final Binder binder = new Binder();
|
||||
private ExecutorService serviceExecutor;
|
||||
private MutableLiveData<DeleteStatus> deleteResultObservable;
|
||||
private MutableLiveData<DeletionFinishedStatus> deleteResultObservable;
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
@@ -59,22 +61,18 @@ public class TrackDeleteService extends Service {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
|
||||
sendResult(null,0, trackIds.size());
|
||||
private void deleteTracks(@NonNull List<Track.Id> trackIds) {
|
||||
serviceExecutor.execute(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
for (int i = 0; i < trackIds.size(); i++) {
|
||||
updateNotification(i + 1, trackIds.size());
|
||||
contentProviderUtils.deleteTrack(this, trackIds.get(i));
|
||||
sendResult(trackIds.get(i), i + 1, trackIds.size());
|
||||
}
|
||||
contentProviderUtils.deleteTracks(this, trackIds);
|
||||
sendResult(trackIds);
|
||||
stopSelf();
|
||||
});
|
||||
}
|
||||
|
||||
private void sendResult(Track.Id trackId, int deletes, int total) {
|
||||
private void sendResult(List<Track.Id> trackIds) {
|
||||
if (deleteResultObservable != null) {
|
||||
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
|
||||
deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +81,7 @@ public class TrackDeleteService extends Service {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public LiveData<DeleteStatus> getDeletingStatusObservable() {
|
||||
public LiveData<DeletionFinishedStatus> getDeletingStatusObservable() {
|
||||
return deleteResultObservable;
|
||||
}
|
||||
|
||||
@@ -106,25 +104,13 @@ public class TrackDeleteService extends Service {
|
||||
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
|
||||
notificationBuilder
|
||||
.setContentTitle(this.getString(R.string.track_delete_progress_message))
|
||||
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
|
||||
.setContentText(this.getString(R.string.track_delete_number_of_tracks, tracksToDelete))
|
||||
.setSmallIcon(R.drawable.ic_logo_color_24dp)
|
||||
.setProgress(tracksToDelete, 0, false);
|
||||
.setProgress(0, 0, true);
|
||||
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates notification progress.
|
||||
*
|
||||
* @param progress number of tracks already deleted.
|
||||
* @param total total of tracks to be deleted.
|
||||
*/
|
||||
private void updateNotification(int progress, int total) {
|
||||
notificationBuilder.setProgress(total, progress, false);
|
||||
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
public class Binder extends android.os.Binder {
|
||||
|
||||
private Binder() {
|
||||
@@ -136,36 +122,25 @@ public class TrackDeleteService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeleteStatus {
|
||||
private final int progress;
|
||||
private final int max;
|
||||
private final Track.Id trackId;
|
||||
public static class DeletionFinishedStatus {
|
||||
private final List<Track.Id> trackIds;
|
||||
|
||||
/**
|
||||
* @param trackId Track.Id just deleted if any.
|
||||
* @param progress number of deletes.
|
||||
* @param max total of deletes to be done.
|
||||
* @param trackIds List of deleted Track.Ids.
|
||||
*/
|
||||
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
|
||||
this.trackId = trackId;
|
||||
this.progress = progress;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return progress == max;
|
||||
private DeletionFinishedStatus(@Nullable List<Track.Id> trackIds) {
|
||||
this.trackIds = trackIds;
|
||||
}
|
||||
|
||||
public boolean isDeleted(Track.Id trackId) {
|
||||
return this.trackId != null && this.trackId.equals(trackId);
|
||||
return this.trackIds != null && this.trackIds.contains(trackId);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DeleteStatus{" +
|
||||
"number of deletes=" + progress +
|
||||
", total=" + max +
|
||||
"trackIds=" + trackIds +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -33,8 +33,8 @@ class VoiceAnnouncementUtils {
|
||||
|
||||
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
Distance distance = trackStatistics.getTotalDistance();
|
||||
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
|
||||
Distance totalDistance = trackStatistics.getTotalDistance();
|
||||
Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed();
|
||||
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
|
||||
|
||||
int perUnitStringId;
|
||||
@@ -68,7 +68,7 @@ class VoiceAnnouncementUtils {
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
double distanceInUnit = distance.toKM_Miles(unitSystem);
|
||||
double distanceInUnit = totalDistance.toKM_Miles(unitSystem);
|
||||
|
||||
if (shouldVoiceAnnounceTotalDistance()) {
|
||||
builder.append(context.getString(R.string.total_distance));
|
||||
@@ -78,7 +78,7 @@ class VoiceAnnouncementUtils {
|
||||
// Punctuation helps introduce natural pauses in TTS
|
||||
builder.append(".");
|
||||
}
|
||||
if (distance.isZero()) {
|
||||
if (totalDistance.isZero()) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class VoiceAnnouncementUtils {
|
||||
|
||||
if (isReportSpeed) {
|
||||
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
||||
double speedInUnit = distancePerTime.to(unitSystem);
|
||||
double speedInUnit = averageMovingSpeed.to(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.speed));
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
|
||||
@@ -108,7 +108,7 @@ class VoiceAnnouncementUtils {
|
||||
}
|
||||
} else {
|
||||
if (shouldVoiceAnnounceAverageSpeedPace()) {
|
||||
Duration time = distancePerTime.toPace(unitSystem);
|
||||
Duration time = averageMovingSpeed.toPace(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.pace));
|
||||
appendDuration(context, builder, time);
|
||||
|
||||
@@ -19,7 +19,7 @@ import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@@ -54,7 +54,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
|
||||
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
|
||||
public void onStop() {
|
||||
if (locationManager != null && context != null) {
|
||||
if (PermissionUtils.hasGPSPermission(context)) {
|
||||
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||
locationManager.removeUpdates(this);
|
||||
}
|
||||
locationManager = null;
|
||||
@@ -152,7 +152,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
|
||||
Log.e(TAG, "Not started.");
|
||||
return;
|
||||
}
|
||||
if (PermissionUtils.hasGPSPermission(context)) {
|
||||
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
|
||||
} catch (SecurityException e) {
|
||||
|
||||
@@ -23,7 +23,7 @@ class GpsStatus {
|
||||
private static final String TAG = GpsStatus.class.getSimpleName();
|
||||
|
||||
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
|
||||
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
|
||||
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);
|
||||
|
||||
private Distance thresholdHorizontalAccuracy;
|
||||
// Threshold for time without points.
|
||||
|
||||
@@ -323,7 +323,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(1)
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
@@ -417,7 +417,8 @@ public class PreferencesUtils {
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.setDecimalCount(1)
|
||||
.setDecimalCount(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
@@ -461,7 +462,8 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(1)
|
||||
.setDecimalCount(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
@@ -534,7 +536,8 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(1)
|
||||
.setDecimalCount(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
|
||||
@@ -826,4 +829,17 @@ public class PreferencesUtils {
|
||||
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
|
||||
}
|
||||
|
||||
public static int getTotalRowsDeleted() {
|
||||
return getInt(R.string.total_rows_deleted_key, 0);
|
||||
}
|
||||
|
||||
public static void addTotalRowsDeleted(final int totalRowsDeletedToAdd) {
|
||||
int newTotalRowsDeleted = getTotalRowsDeleted() + totalRowsDeletedToAdd;
|
||||
setInt(R.string.total_rows_deleted_key, newTotalRowsDeleted);
|
||||
}
|
||||
|
||||
public static void resetTotalRowsDeleted() {
|
||||
setInt(R.string.total_rows_deleted_key, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@ import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeRunningSpeedAndCadencePreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
|
||||
|
||||
public class SensorsSettingsFragment extends PreferenceFragmentCompat {
|
||||
@@ -34,18 +29,8 @@ public class SensorsSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onDisplayPreferenceDialog(Preference preference) {
|
||||
DialogFragment dialogFragment = null;
|
||||
if (preference instanceof BluetoothLeHeartRatePreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES);
|
||||
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
|
||||
} else if (preference instanceof BluetoothLeCyclingPowerPreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_POWER_UUID);
|
||||
} else if (preference instanceof BluetoothLeRunningSpeedAndCadencePreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID);
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
if (preference instanceof BluetoothLeSensorPreference) {
|
||||
DialogFragment dialogFragment = ((BluetoothLeSensorPreference) preference).createInstance();
|
||||
dialogFragment.setTargetFragment(this, 0);
|
||||
dialogFragment.show(getParentFragmentManager(), getClass().getSimpleName());
|
||||
return;
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class BluetoothLeCyclingCadenceAndSpeedPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
|
||||
public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeCyclingCadencePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadencePreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadencePreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadencePreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_CADENCE);
|
||||
}
|
||||
}
|
||||
+9
@@ -3,6 +3,10 @@ package de.dennisguse.opentracks.settings.bluetooth;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
|
||||
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@@ -20,4 +24,9 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
|
||||
public BluetoothLeCyclingPowerPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_POWER);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
|
||||
public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeCyclingSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingSpeedPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingSpeedPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE);
|
||||
}
|
||||
}
|
||||
+9
@@ -3,6 +3,10 @@ package de.dennisguse.opentracks.settings.bluetooth;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
|
||||
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@@ -20,4 +24,9 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference
|
||||
public BluetoothLeHeartRatePreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES);
|
||||
}
|
||||
}
|
||||
+9
@@ -3,6 +3,10 @@ package de.dennisguse.opentracks.settings.bluetooth;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
|
||||
public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeRunningSpeedAndCadencePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@@ -20,4 +24,9 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens
|
||||
public BluetoothLeRunningSpeedAndCadencePreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreferenceDialogFragmentCompat createInstance() {
|
||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.RUNNING_SPEED_CADENCE);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-19
@@ -23,13 +23,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
/**
|
||||
* Preference to select a discoverable Bluetooth LE device.
|
||||
@@ -92,6 +92,8 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
public abstract PreferenceDialogFragmentCompat createInstance();
|
||||
|
||||
public static class BluetoothLeSensorPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
private AnimatedVectorDrawableCompat bluetoothIcon;
|
||||
@@ -123,15 +125,18 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
}
|
||||
};
|
||||
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, UUID sensorUUID) {
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, ServiceMeasurementUUID sensorUUID) {
|
||||
return newInstance(preferenceKey, Collections.singletonList(sensorUUID));
|
||||
}
|
||||
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<UUID> sensorUUIDs) {
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<ServiceMeasurementUUID> sensorUUIDs) {
|
||||
final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog();
|
||||
final Bundle b = new Bundle(1);
|
||||
b.putString(ARG_KEY, preferenceKey);
|
||||
b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream().map(ParcelUuid::new).collect(Collectors.toList())));
|
||||
b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream()
|
||||
.map(ServiceMeasurementUUID::getServiceUUID)
|
||||
.map(ParcelUuid::new)
|
||||
.collect(Collectors.toList())));
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
@@ -145,20 +150,18 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
||||
bluetoothIcon.start();
|
||||
|
||||
if (!PermissionUtils.hasBluetoothPermissions(getContext())) {
|
||||
PermissionUtils.requestBluetoothPermission(this,
|
||||
this::startBluetoothScan,
|
||||
() -> {
|
||||
if (PermissionUtils.shouldShowRequestPermissionRationaleBluetooth(this)) {
|
||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
dismiss();
|
||||
});
|
||||
} else {
|
||||
startBluetoothScan();
|
||||
}
|
||||
PermissionRequester.BLUETOOTH.requestPermissionsIfNeeded(getContext(), this,
|
||||
this::startBluetoothScan,
|
||||
(requester) -> {
|
||||
if (requester.shouldShowRequestPermissionRationale(this)) {
|
||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
dismiss();
|
||||
});
|
||||
|
||||
startBluetoothScan();
|
||||
}
|
||||
|
||||
private void startBluetoothScan() {
|
||||
|
||||
@@ -30,7 +30,6 @@ import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
@@ -154,7 +153,7 @@ public class DescriptionGenerator {
|
||||
}
|
||||
|
||||
// Recorded time
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(OffsetDateTime.ofInstant(stats.getStartTime(), ZoneId.systemDefault()))));
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(stats.getStartTime(), ZoneId.systemDefault()))));
|
||||
builder.append(lineBreak);
|
||||
|
||||
return builder.toString();
|
||||
@@ -230,7 +229,7 @@ public class DescriptionGenerator {
|
||||
@VisibleForTesting
|
||||
void writeAltitude(double altitude_m, StringBuilder builder, int resId, String lineBreak) {
|
||||
long altitudeInM = Math.round(altitude_m);
|
||||
long altitudeInFt = Math.round(altitude_m * UnitConversions.M_TO_FT);
|
||||
long altitudeInFt = Math.round(Distance.of(altitude_m).toFT());
|
||||
builder.append(context.getString(resId, altitudeInM, altitudeInFt));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ 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.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
|
||||
/**
|
||||
* Updater for {@link TrackStatistics}.
|
||||
@@ -196,7 +195,7 @@ public class TrackStatisticsUpdater {
|
||||
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
|
||||
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
Speed maxSpeedDifference = Speed.of(Distance.of(SPEED_MAX_ACCELERATION), Duration.ofMillis(1000))
|
||||
.mul(timeDifference.toMillis() / UnitConversions.S_TO_MS);
|
||||
.mul(timeDifference.toSeconds());
|
||||
|
||||
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
|
||||
return speedDifference.lessThan(maxSpeedDifference);
|
||||
|
||||
@@ -108,7 +108,11 @@ public class ListItemUtils {
|
||||
String timeValue = null;
|
||||
if (!isRecording) {
|
||||
dateValue = StringUtils.formatDateTodayRelative(context, offsetDateTime);
|
||||
timeValue = offsetDateTime.format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
String pattern = "HH:mm";
|
||||
if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) {
|
||||
pattern = "HH:mm x";
|
||||
}
|
||||
timeValue = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
setTextView(context, dateTextView, dateValue, hasPhoto);
|
||||
setTextView(context, timeTextView, timeValue, hasPhoto);
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PermissionRequester {
|
||||
|
||||
private final List<String> permissions;
|
||||
|
||||
public PermissionRequester(List<String> permissions) {
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
public boolean hasPermission(Context context) {
|
||||
return permissions.stream()
|
||||
.map(p -> ContextCompat.checkSelfPermission(context, p))
|
||||
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
public void requestPermissionsIfNeeded(Context context, ActivityResultCaller caller, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
|
||||
if (!hasPermission(context)) {
|
||||
requestPermission(caller, onGranted, onRejected);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldShowRequestPermissionRationale(Fragment context) {
|
||||
return permissions.stream()
|
||||
.anyMatch(context::shouldShowRequestPermissionRationale);
|
||||
}
|
||||
|
||||
private void requestPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
|
||||
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
|
||||
boolean isGranted = permissions.stream()
|
||||
.allMatch(p -> result.getOrDefault(p, false));
|
||||
if (isGranted && onGranted != null) {
|
||||
onGranted.run();
|
||||
}
|
||||
if (!isGranted && onRejected != null) {
|
||||
onRejected.rejected(this);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
locationPermissionRequest.launch(permissions.toArray(permissions.toArray(new String[0])));
|
||||
}
|
||||
|
||||
private static final List<String> GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
|
||||
private static final List<String> BLUETOOTH_PERMISSIONS;
|
||||
|
||||
static {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BLUETOOTH_PERMISSIONS = List.of(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT);
|
||||
} else {
|
||||
BLUETOOTH_PERMISSIONS = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<String> NOTIFICATION_PERMISSIONS;
|
||||
|
||||
static {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
NOTIFICATION_PERMISSIONS = List.of(Manifest.permission.POST_NOTIFICATIONS);
|
||||
} else {
|
||||
NOTIFICATION_PERMISSIONS = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<String> ALL_PERMISSIONS;
|
||||
|
||||
static {
|
||||
ArrayList<String> all = new ArrayList<>(GPS_PERMISSION);
|
||||
all.addAll(BLUETOOTH_PERMISSIONS);
|
||||
all.addAll(NOTIFICATION_PERMISSIONS);
|
||||
|
||||
ALL_PERMISSIONS = Collections.unmodifiableList(all);
|
||||
}
|
||||
|
||||
public final static PermissionRequester GPS = new PermissionRequester(GPS_PERMISSION);
|
||||
public final static PermissionRequester BLUETOOTH = new PermissionRequester(BLUETOOTH_PERMISSIONS);
|
||||
public final static PermissionRequester NOTIFICATION = new PermissionRequester(NOTIFICATION_PERMISSIONS);
|
||||
|
||||
public final static PermissionRequester ALL = new PermissionRequester(ALL_PERMISSIONS);
|
||||
|
||||
public interface RejectedCallback {
|
||||
void rejected(PermissionRequester permissionRequester);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PermissionUtils {
|
||||
|
||||
private static final String[] GPS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
|
||||
private static final String[] BLUETOOTH;
|
||||
|
||||
static {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
BLUETOOTH = new String[]{};
|
||||
} else {
|
||||
BLUETOOTH = new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT};
|
||||
}
|
||||
}
|
||||
|
||||
private PermissionUtils() {
|
||||
}
|
||||
|
||||
public static boolean hasGPSPermission(Context context) {
|
||||
return hasPermissions(context, GPS);
|
||||
}
|
||||
|
||||
public static void requestGPSPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
||||
requestPermission(context, GPS, onGranted, onRejected);
|
||||
}
|
||||
|
||||
public static boolean shouldShowRequestPermissionRationaleBluetooth(Fragment context) {
|
||||
return Arrays.stream(BLUETOOTH).anyMatch(context::shouldShowRequestPermissionRationale);
|
||||
}
|
||||
|
||||
public static boolean hasBluetoothPermissions(Context context) {
|
||||
return hasPermissions(context, BLUETOOTH);
|
||||
}
|
||||
|
||||
public static void requestBluetoothPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
||||
requestPermission(context, BLUETOOTH, onGranted, onRejected);
|
||||
}
|
||||
|
||||
private static void requestPermission(ActivityResultCaller context, final String[] permissions, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
|
||||
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
|
||||
boolean isGranted = Arrays.stream(permissions)
|
||||
.allMatch(p -> result.getOrDefault(p, false));
|
||||
if (isGranted && onGranted != null) {
|
||||
onGranted.run();
|
||||
}
|
||||
if (!isGranted && onRejected != null) {
|
||||
onRejected.run();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
locationPermissionRequest.launch(permissions);
|
||||
}
|
||||
|
||||
private static boolean hasPermissions(Context context, String[] permissions) {
|
||||
return Arrays.stream(permissions)
|
||||
.map(p -> ContextCompat.checkSelfPermission(context, p))
|
||||
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
}
|
||||
@@ -61,13 +61,6 @@ public class StringUtils {
|
||||
private StringUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the date and time of the OffsetDateTime (using default Locale format)
|
||||
*/
|
||||
public static String formatDateTime(OffsetDateTime odt) {
|
||||
return odt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the date and time with the offset (using default Locale format).
|
||||
*/
|
||||
@@ -75,6 +68,13 @@ public class StringUtils {
|
||||
return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL));
|
||||
}
|
||||
|
||||
public static String formatDateTimeWithOffsetIfDifferent(OffsetDateTime odt) {
|
||||
if (!odt.getOffset().equals(OffsetDateTime.now().getOffset())) {
|
||||
return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL));
|
||||
}
|
||||
return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
|
||||
}
|
||||
|
||||
public static String formatLocalDateTime(LocalDateTime localDateTime) {
|
||||
return localDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL));
|
||||
}
|
||||
@@ -257,7 +257,8 @@ public class StringUtils {
|
||||
//TODO altitude_m should be double or a value object
|
||||
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(1)
|
||||
.setDecimalCount(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
|
||||
|
||||
@@ -26,12 +26,17 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@Deprecated //TODO Refactor: all this should happen somewhere else (ContentProviderUtils?)
|
||||
public class TrackUtils {
|
||||
|
||||
private TrackUtils() {
|
||||
}
|
||||
|
||||
public static void updateTrack(Context context, Track track, String name, String category, String description, ContentProviderUtils contentProviderUtils) {
|
||||
updateTrack(context, track, name, category, TrackIconUtils.getIconValue(context, category), description, contentProviderUtils);
|
||||
}
|
||||
|
||||
public static void updateTrack(Context context, Track track, String name, String category, String iconValue, String description, ContentProviderUtils contentProviderUtils) {
|
||||
boolean update = false;
|
||||
if (name != null) {
|
||||
track.setName(name);
|
||||
@@ -39,9 +44,13 @@ public class TrackUtils {
|
||||
}
|
||||
if (category != null) {
|
||||
track.setCategory(category);
|
||||
track.setIcon(TrackIconUtils.getIconValue(context, category));
|
||||
update = true;
|
||||
}
|
||||
if (iconValue != null) {
|
||||
track.setIcon(iconValue);
|
||||
} else if (category != null){
|
||||
track.setIcon(TrackIconUtils.getIconValue(context, category));
|
||||
}
|
||||
if (description != null) {
|
||||
track.setDescription(description);
|
||||
update = true;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
|
||||
<group
|
||||
android:name="rotationGroup"
|
||||
android:pivotX="50"
|
||||
android:pivotY="50"
|
||||
android:scaleX="0.6"
|
||||
android:scaleY="0.6"
|
||||
>
|
||||
<path android:pathData="M82.489,12.834C83.196,10.418 82.043,9.556 79.926,10.917L10.087,55.828c-2.117,1.361 -1.795,2.628 0.714,2.815L39.806,60.807c2.51,0.188 5.291,2.267 6.18,4.621l10.282,27.208c0.89,2.354 2.195,2.304 2.902,-0.111z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="70.0"
|
||||
android:endY="30.0"
|
||||
android:startX="7.0"
|
||||
android:startY="83.0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFBB22F"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFFF6D00"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:opacity="opaque">
|
||||
|
||||
<item android:drawable="@color/splashscreen_background" />
|
||||
|
||||
<item
|
||||
android:width="300dp"
|
||||
android:height="300dp"
|
||||
android:gravity="center"
|
||||
tools:targetApi="m">
|
||||
<inset
|
||||
android:drawable="@drawable/ic_logo_color_24dp"
|
||||
android:inset="20dp" />
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2010 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
--><resources>
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">طائرة</string>
|
||||
<string name="activity_type_atv">ATV</string>
|
||||
@@ -200,14 +200,15 @@ limitations under the License.
|
||||
<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لعرض هذا المسار، استخدام برنامج Google Earth أو غيره من البرامج تصميم الخرائط.\n\n%1$s</string>
|
||||
<string name="share_track_share_file_body">أعتقد أنك قد تكون مهتمًا بهذا المسار.
|
||||
\n
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">أريد مشاركة مسار معك</string>
|
||||
<!-- Stats -->
|
||||
<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>
|
||||
@@ -352,4 +353,4 @@ limitations under the License.
|
||||
<string name="waypoint_type_waterfall">شلال</string>
|
||||
<string name="waypoint_type_water_fountain">نافورة مياه</string>
|
||||
<string name="waypoint_type_waypoint">نقطة طريق</string>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -411,7 +411,6 @@
|
||||
<string name="stats_split_interval">Intervalo partido</string>
|
||||
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="track_detail_intervals_tab">Intervalos</string>
|
||||
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
|
||||
<string name="settings_recording_customize_layout_title">Personalice su pantalla de grabación</string>
|
||||
|
||||
@@ -458,7 +458,6 @@ limitations under the License.
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показване на статистически данни на заключения екран</string>
|
||||
<string name="help_data_google_play_services_title">Изисква ли OpenTracks/използва ли услугата Google Play\?</string>
|
||||
<string name="about_version_code">Код на версията: %1$d</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="export_error_post_workout">Експортът след тренировка е неуспешен, моля, проверете директорията за експорт.</string>
|
||||
<string name="about_third_party_data">Данни от трети страни</string>
|
||||
<string name="import_unsupported_format">Неподдържан формат на файла</string>
|
||||
|
||||
@@ -469,7 +469,6 @@ limitations under the License.
|
||||
<string name="track_speed_notification">Rychlost: %1$s</string>
|
||||
<string name="track_pace_notification">Rychlost: %1$s</string>
|
||||
<string name="about_third_party_data">Údaje třetích stran</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="delete_service">Služba mazání</string>
|
||||
<string name="menu_voice_rate">Rychlost hlasových oznámení</string>
|
||||
<string name="track_delete_not_recording">Není možno vymazat aktuálně zaznamenávanou trasu.</string>
|
||||
|
||||
@@ -467,7 +467,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="track_speed_notification">Hastighed: %1$s</string>
|
||||
<string name="about_third_party_data">Data fra tredjeparter</string>
|
||||
<string name="delete_service">Tjeneste til sletning</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Stemmehastighed</string>
|
||||
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er optaget i øjeblikket.</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Afstand og hastighed</string>
|
||||
|
||||
@@ -276,9 +276,8 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
\n%1$s</string>
|
||||
<string name="share_track_subject">Ich möchte einen Track mit dir teilen</string>
|
||||
<!-- Stats -->
|
||||
<string name="stats_average_moving_pace">Durchschnitt\n(Bewegung)</string>
|
||||
<string name="stats_average_moving_speed">Durchschnittsgeschwindigkeit
|
||||
\n(Bewegung)</string>
|
||||
<string name="stats_average_moving_pace">Durchschnittliches Bewegungstempo</string>
|
||||
<string name="stats_average_moving_speed">Mittlere Geschwindigkeit in Bewegung</string>
|
||||
<string name="stats_average_pace">Durchschnitt</string>
|
||||
<string name="stats_average_speed">Durchschnittsgeschwindigkeit</string>
|
||||
<string name="stats_altitude">Höhe</string>
|
||||
@@ -464,13 +463,12 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
||||
<string name="about_third_party_data">Daten von Drittherstellern</string>
|
||||
<string name="sensor_state_heart_rate_max">Maximaler Herzfrequenz</string>
|
||||
<string name="track_delete_not_recording">Dieser Track kann nicht gelöscht werden, da er gerade aufgezeichnet wird.</string>
|
||||
<string name="interval_list_empty_message">Es gibt noch keine Intervalle.</string>
|
||||
<string name="interval_list_empty_message">Es gibt noch keine Intervalle</string>
|
||||
<string name="import_parser_error">Parser Fehler: %1$s</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Anzeige im Vollbildmodus während der Aufzeichnung.</string>
|
||||
<string name="sensor_state_heart_rate_avg">Durchschnittlicher Herzfrequenz</string>
|
||||
<string name="settings_prevent_reimport_tracks_title">Verhindere den erneuten Import von Strecken(aufzeichnungen)</string>
|
||||
<string name="import_no_kml_file_found">Keine doc.kml Datei im KMZ gefunden</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="import_thread_interrupted">Thread unterbrochen</string>
|
||||
<string name="import_unable_to_import_file">Importieren der Datei gescheitert: %1$s</string>
|
||||
<string name="delete_service">Löschdienst</string>
|
||||
|
||||
@@ -464,7 +464,6 @@ limitations under the License.
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="about_third_party_data">Datos de terceros</string>
|
||||
<string name="delete_service">Borrar servicio</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Velocidad de la voz</string>
|
||||
<string name="track_delete_not_recording">Este recorrido no puede ser eliminado porque se está grabando actualmente.</string>
|
||||
<string name="settings_sensor_running_speed_and_cadence">Distancia, velocidad y cadencia</string>
|
||||
|
||||
@@ -464,7 +464,6 @@ limitations under the License.
|
||||
<string name="track_pace_notification">Tempo: %1$s</string>
|
||||
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
|
||||
<string name="delete_service">Kustutamise teenus</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Hääle kiirus</string>
|
||||
<string name="track_delete_not_recording">Seda pala ei saa kustutada, kuna see on praegu salvestatud.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Rütm</string>
|
||||
|
||||
@@ -467,7 +467,6 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
|
||||
<string name="track_pace_notification">Erritmoa: %1$s</string>
|
||||
<string name="about_third_party_data">Hirugarrenen datuak</string>
|
||||
<string name="delete_service">Ezabatzeko zerbitzua</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Ahots abiadura</string>
|
||||
<string name="track_delete_not_recording">Pista hau ezin da ezabatu da gaur egun grabatzen.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Kadentzia</string>
|
||||
|
||||
@@ -469,7 +469,6 @@ limitations under the License.
|
||||
<string name="track_speed_notification">Nopeus: %1$s</string>
|
||||
<string name="about_third_party_data">Kolmannen osapuolen tiedot</string>
|
||||
<string name="delete_service">Poistopalvelu</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Äänen nopeus</string>
|
||||
<string name="track_delete_not_recording">Tätä reittiä ei voi poistaa koska sitä tallennetaan parhaillaan.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Pyöräily: tahtiaika</string>
|
||||
|
||||
@@ -465,7 +465,6 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
|
||||
<string name="export_track_already_exists_msg">La trace %1$s existe déjà dans le répertoire de destination.</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Traces écrasées</string>
|
||||
<string name="about_third_party_data">Données de tiers</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="delete_service">Service de suppression</string>
|
||||
<string name="menu_voice_rate">Vitesse vocale</string>
|
||||
<string name="track_delete_not_recording">Cette piste ne peut pas être supprimée car elle est en cours d\'enregistrement.</string>
|
||||
@@ -606,4 +605,6 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
|
||||
<item quantity="one">%1$.2f mile nautique</item>
|
||||
<item quantity="other">%1$.2f milles nautique</item>
|
||||
</plurals>
|
||||
<string name="settings_public_api_package_title">Nom du paquet pour l\'API Publique</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d parcours</string>
|
||||
</resources>
|
||||
@@ -45,11 +45,8 @@ limitations under the License.
|
||||
<string name="help_recording_content">
|
||||
OpenTracks usa o sensor GPS do dispositivo para gravar a localización de xeito contínuo.
|
||||
Se o GPS informa sobre datos pouco precisos (ex.: localización, velocidade, elevación), OpenTracks non pode facer nada ao respecto.</string>
|
||||
<string name="help_data_storage_title">¿De que xeito garda OpenTracks os datos dos sensores?</string>
|
||||
<string name="help_data_storage_content">
|
||||
Os datos dos sensores gárdanse asociados a localización.
|
||||
Respecto do ritmo cardíaco, significa que só se garda <i>unha medida</i> asociada a unha localización.
|
||||
Se te detés ou estás en interiores, non se gardarán datos do sensor (mais serán mostrados). </string>
|
||||
<string name="help_data_storage_title">De que xeito garda OpenTracks os datos dos sensores\?</string>
|
||||
<string name="help_data_storage_content"> Os datos dos sensores gárdanse asociados á localización. Respecto do ritmo cardíaco, significa que só se garda <i>unha medida</i> asociada a unha localización. Se te detés ou estás en interiores, non se gardarán datos do sensor (mais serán mostrados). </string>
|
||||
<string name="help_gps_troubleshooting_title">¿OpenTracks non grava as localizacións?</string>
|
||||
<string name="help_gps_troubleshooting_content"> Podes intentar:
|
||||
\n a) mira que o GPS esté activado,
|
||||
@@ -248,35 +245,35 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="settings_chart_by_distance">Por distancia</string>
|
||||
<string name="settings_chart_by_time">Por duración</string>
|
||||
<!-- Settings Recording -->
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Mostrar estatísticas na pantalla de bloqueo</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Estatísticas na pantalla de bloqueo</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Ao gravar, mostra estatísticas sen desbloquear o dispositivo.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Manter pantalla acesa</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Ao gravar, manten a pantalla acesa.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Ao gravar, mantén a pantalla acesa.</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_title">Pantalla completa</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Mentras grava, mostrar a pantalla completa.</string>
|
||||
<string name="settings_recording_default_activity_title">Tipo de actividade predeterminada</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s entre as situacións gravadas</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Créase un novo segmento se a distancia entre as situacións é superior a %1$s</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s entre as posicións gravadas</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Créase un novo segmento se a distancia entre as posicións é superior a %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Distancia máxima</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Distancia entre situacións</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Tempo entre situacións</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Intervalo da distancia para gravar</string>
|
||||
<string name="settings_recording_min_recording_interval_title">Tempo entre posicións</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Descoñécese se a precisión é inferior a %1$s</string>
|
||||
<string name="settings_recording_min_required_accuracy_title">Precisión do GPS</string>
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Data (local)</string>
|
||||
<string name="settings_recording_track_name_number_option">Número</string>
|
||||
<string name="settings_recording_track_name_title">Nome da ruta por omisión</string>
|
||||
<string name="settings_recording_track_name_title">Nome por defecto para a ruta</string>
|
||||
<!-- Settings Reset -->
|
||||
<string name="settings_reset_confirm_message">Toda a configuración volverá aos valores predeterminados.</string>
|
||||
<string name="settings_reset_confirm_message">Toda a configuración volverá aos valores predeterminados. Esto non elimina as rutas no dispotivo.</string>
|
||||
<string name="settings_reset_confirm_title">Restablecer agora?</string>
|
||||
<string name="settings_reset_done">Restablecéronse todos os valores predeterminados da configuración</string>
|
||||
<string name="settings_reset_done">Restableceuse a configuración aos valores por defecto</string>
|
||||
<!-- Settings Sensor -->
|
||||
<string name="settings_sensor_bluetooth">Bluetooth</string>
|
||||
<string name="settings_sensor_wheel_circumference">Circunferencia da roda (mm)</string>
|
||||
<!-- Settings Stats -->
|
||||
<string name="settings_stats_rate_title">Indicador preferido</string>
|
||||
<string name="settings_stats_units_imperial">Imperiais (millas, pés)</string>
|
||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||
<string name="settings_stats_units_metric">Unidades (km, m)</string>
|
||||
<string name="settings_stats_units_title">Unidades preferidas</string>
|
||||
<!-- Share Track -->
|
||||
<string name="share_track_share_file_body">Creo que che pode interesar esta ruta.
|
||||
@@ -472,7 +469,6 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="track_speed_notification">Velocidade: %1$s</string>
|
||||
<string name="about_third_party_data">Datos de terceiros alleos</string>
|
||||
<string name="delete_service">Eliminación de servizo</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Velocidade da voz</string>
|
||||
<string name="track_delete_not_recording">Esta pista non se pode eliminar porque está a funcionar a gravación.</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Distancia e Velocidade</string>
|
||||
@@ -505,7 +501,7 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="settings_announcements_distance_frequency">Intervalo de distancia</string>
|
||||
<string name="permission_bluetooth_failed">OpenTracks require permiso para usar o Bluetooth.</string>
|
||||
<string name="permission_bluetooth_failed_rejected">Tes que permitirlle a OpenTracks nos axustes do sistema que acceda a Dispositivos Próximos.</string>
|
||||
<string name="settings_recording_idle_speed_title">Ocioso límite de velocidade</string>
|
||||
<string name="settings_recording_idle_speed_title">Limiar detección movemento</string>
|
||||
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
|
||||
<string name="value_float_mile_hour">%1$.1f km/h</string>
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f km/h (recomendado)</string>
|
||||
@@ -516,17 +512,17 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<string name="settings_announcements_summary">A Distancia, Tempo, Voz Velocidade</string>
|
||||
<string name="settings_open_tracks_summary">Todo Sobre OpenTracks Proxecto</string>
|
||||
<string name="settings_defaults_title">Por defecto Unidades e Actividade</string>
|
||||
<string name="settings_ui_title">Interface De Usuario</string>
|
||||
<string name="settings_ui_title">Interface da app</string>
|
||||
<string name="settings_ui_summary">Aparencia, Gravación, Deseño, Tema</string>
|
||||
<string name="settings_gps_title">GPS</string>
|
||||
<string name="settings_gps_summary">Intervalo de tempo, Distancia intervalo, Max Distancia, Precisión</string>
|
||||
<string name="settings_sensors_title">Bluetooth Sensores</string>
|
||||
<string name="settings_sensors_summary">Ciclismo Sensores, Roda Circunferencia, Execución De Sensores</string>
|
||||
<string name="settings_announcements_title">Voz Anuncios</string>
|
||||
<string name="settings_sensors_title">Sensores Bluetooth</string>
|
||||
<string name="settings_sensors_summary">Sensores de ciclismo, Circunferencia da roda, Sensores de carreira</string>
|
||||
<string name="settings_announcements_title">Indicacións por Voz</string>
|
||||
<string name="settings_import_export_title">Importación/Exportación</string>
|
||||
<string name="settings_import_export_summary">Importación, Exportación, Auto Exportación</string>
|
||||
<string name="settings_units_category">Unidades</string>
|
||||
<string name="settings_activity_defaults_category">Actividade de Inadimplência</string>
|
||||
<string name="settings_activity_defaults_category">Valores por defecto</string>
|
||||
<string name="settings_reset_summary">Axustar a Configuración de Valores por Defecto</string>
|
||||
<string name="menu_select_layout">Elixe disposición</string>
|
||||
<string name="custom_layout_list_title">Disposicións</string>
|
||||
@@ -601,15 +597,17 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
|
||||
<item quantity="one">1 milla náutica</item>
|
||||
<item quantity="other">%1$.2f millas náuticas</item>
|
||||
</plurals>
|
||||
<string name="show_on_dashboard">Taboleiro da API OpenTracks</string>
|
||||
<string name="show_on_dashboard">Usar API OpenTracks</string>
|
||||
<plurals name="voiceSpeedMKnots">
|
||||
<item quantity="one">1 nó</item>
|
||||
<item quantity="other">%1$.1f nós</item>
|
||||
</plurals>
|
||||
<string name="show_on_dashboard_not_installed">Non hai ningunha aplicación instalada con soporte para a API do taboleiro OpenTracks.</string>
|
||||
<string name="show_on_dashboard_not_installed">Non hai ningunha aplicación instalada con soporte para a API de OpenTracks.</string>
|
||||
<string name="select_show_on_map_behavior">Elixe \"ver no mapa\"</string>
|
||||
<string name="show_on_map_format_ask">Preguntar</string>
|
||||
<string name="always">Sempre</string>
|
||||
<string name="error_crash_title">%1$s fallou</string>
|
||||
<string name="report_bug">Informar do problema</string>
|
||||
<string name="settings_public_api_package_title">Nome do paquete para Public API</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d rutas</string>
|
||||
</resources>
|
||||
@@ -460,7 +460,6 @@ limitations under the License.
|
||||
\n * pályák megjelenítése térképen (lásd alább)</string>
|
||||
<string name="about_third_party_data">Harmadik féltől származó adatok</string>
|
||||
<string name="delete_service">Törlési szolgáltatás</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Hang sebesség</string>
|
||||
<string name="track_delete_not_recording">Ez a sáv nem törölhető, mivel jelenleg is rögzítve van.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Kerékpározás: lépésszám</string>
|
||||
|
||||
@@ -471,7 +471,6 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="about_third_party_data">Dati di terzi</string>
|
||||
<string name="delete_service">Servizio di eliminazione</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Velocità della voce</string>
|
||||
<string name="track_delete_not_recording">Questa traccia non può essere cancellata perché è attualmente in registrazione.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Cadenza</string>
|
||||
|
||||
@@ -416,7 +416,6 @@ limitations under the License.
|
||||
<string name="help_recording_content">OpenTracks naudoja prietaiso GPS jutiklį, kad nuolat įrašytų vietą. Jei GPS įrenginys praneša apie netikslius duomenis (pvz., Vietą, greitį, aukštį), OpenTracks negali nieko padaryti.</string>
|
||||
<string name="generic_open_track">Atidaryti takelį</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Įrašydami palaikykite ekraną įjungtą.</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="open_track_as_GPX">Atidaryti kaip GPX</string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks nefiksuoja vietų\?</string>
|
||||
<string name="generic_overwrite">Perrašyti</string>
|
||||
|
||||
@@ -416,7 +416,6 @@ limitations under the License.
|
||||
<string name="help_track_export_title">Kā es varu eksportēt/ kopīgot ierakstītos ierakstus\?</string>
|
||||
<string name="instant_export_enabled_summary">Dziesmas eksportēšana uz krātuvi pēc ierakstīšanas pabeigšanas</string>
|
||||
<string name="activity_type_escooter">eScooter</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="sensor_state_power_avg">Vidējā jauda</string>
|
||||
<string name="about_libraries">Bibliotēkas</string>
|
||||
<string name="description_default_speed_or_pace">Pēc aktivitātes veida</string>
|
||||
|
||||
@@ -178,7 +178,7 @@ limitations under the License.
|
||||
<string name="settings_chart_by_distance">Etter distanse</string>
|
||||
<string name="settings_chart_by_time">Etter tid</string>
|
||||
<string name="settings_recording_default_activity_title">Standard aktivitetstype</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s mellom innspilte posisjoner</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s mellom registrerte posisjoner</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Det blir opprettet et nytt segment hvis avstanden mellom to posisjoner er større enn %1$s</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Maks avstand</string>
|
||||
<string name="settings_recording_min_recording_distance_title">Avstandsintervall for opptak</string>
|
||||
@@ -460,7 +460,6 @@ limitations under the License.
|
||||
<string name="track_pace_notification">Tempo: %1$s</string>
|
||||
<string name="about_third_party_data">Tredjepartsdata</string>
|
||||
<string name="delete_service">Slettingstjeneste</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Talehastighet</string>
|
||||
<string name="track_delete_not_recording">Dette sporet kan ikke slettes da det tas opp nå.</string>
|
||||
<string name="settings_sensor_cycling_cadence">Kadens</string>
|
||||
@@ -600,4 +599,6 @@ limitations under the License.
|
||||
<item quantity="other">%1$.2f nautiske mil</item>
|
||||
</plurals>
|
||||
<string name="show_on_dashboard_not_installed">Ingen app som støtter OpenTracks Dashboard API er installert.</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d spor</string>
|
||||
<string name="settings_public_api_package_title">Pakkenavn for Offentlig API</string>
|
||||
</resources>
|
||||
@@ -465,7 +465,6 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
|
||||
<string name="track_distance_notification">Afstand: %1$s</string>
|
||||
<string name="track_pace_notification">Tempo: %1$s</string>
|
||||
<string name="about_third_party_data">Gegevens van derden</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="delete_service">Dienst verwijdering</string>
|
||||
<string name="menu_voice_rate">Spraak snelheid</string>
|
||||
<string name="track_delete_not_recording">Dit fragment kan niet worden gewist omdat het momenteel is opgenomen.</string>
|
||||
|
||||
@@ -476,7 +476,6 @@ limitations under the License.
|
||||
<string name="track_distance_notification">Odległość: %1$s</string>
|
||||
<string name="about_third_party_data">Dane osób trzecich</string>
|
||||
<string name="delete_service">Usługa usuwania</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Szybkość głosu</string>
|
||||
<string name="track_delete_not_recording">Tej ścieżki nie można usunąć, ponieważ jest ona aktualnie nagrywana.</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Dystans i Prędkość</string>
|
||||
|
||||
@@ -404,7 +404,6 @@
|
||||
<string name="track_pace_notification">Parcela: %1$s</string>
|
||||
<string name="about_third_party_data">Dados de terceiros</string>
|
||||
<string name="delete_service">Serviço de exclusão</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="menu_voice_rate">Velocidade da voz</string>
|
||||
<string name="track_delete_not_recording">Esta trilha não pode ser excluída porque está gravada no momento.</string>
|
||||
<plurals name="voiceSpeedKilometersPerHour">
|
||||
|
||||
@@ -426,7 +426,6 @@ limitations under the License.
|
||||
<string name="import_progress_summary_exists_msg">Există deja</string>
|
||||
<string name="track_detail_intervals_tab">Intervaluri</string>
|
||||
<string name="track_recording_notification_accuracy">Precizia locației: %1$s</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="sensor_state_cadence_max">Max Cadență</string>
|
||||
<string name="sensor_could_not_scan">Nu a fost posibilă scanarea pentru dispozitive Bluetooth (eroare %1$i).</string>
|
||||
<string name="share_image_subject">Aș dori să vă împărtășesc o imagine</string>
|
||||
|
||||
@@ -24,7 +24,7 @@ limitations under the License.
|
||||
<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_dirt_bike">Дертбайк</string>
|
||||
<string name="activity_type_donkey_back_riding">Верховая езда на ослах</string>
|
||||
<string name="activity_type_driving">на автомобиле</string>
|
||||
<string name="activity_type_driving_bus">Вождение автобуса</string>
|
||||
@@ -54,7 +54,7 @@ limitations under the License.
|
||||
<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_road_biking">Шоссейный велосипед</string>
|
||||
<string name="activity_type_roller_skiing">роликовые лыжи</string>
|
||||
<string name="activity_type_rowing">Гребля академическая</string>
|
||||
<string name="activity_type_running">Бег</string>
|
||||
@@ -128,7 +128,7 @@ limitations under the License.
|
||||
<string name="image_arrow">Стрелка</string>
|
||||
<string name="image_marker">Маркер</string>
|
||||
<string name="image_marker_count">Количество маркеров</string>
|
||||
<string name="image_photo_marker">Маркер фотографии</string>
|
||||
<string name="image_photo_marker">Фотомаркер</string>
|
||||
<string name="image_record">Запись</string>
|
||||
<string name="image_stop">Стоп</string>
|
||||
<string name="image_track">Трек</string>
|
||||
@@ -212,7 +212,7 @@ limitations under the License.
|
||||
<string name="stats_average_speed">Средняя скорость</string>
|
||||
<string name="stats_altitude">Высота</string>
|
||||
<string name="stats_fastest_pace">Макс. темп</string>
|
||||
<string name="stats_gain">Рост</string>
|
||||
<string name="stats_gain">Набор высоты</string>
|
||||
<string name="stats_latitude">Широта</string>
|
||||
<string name="stats_longitude">Долгота</string>
|
||||
<string name="stats_max_speed">Макс. скорость</string>
|
||||
@@ -337,53 +337,53 @@ limitations under the License.
|
||||
<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_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"> Используйте значок маленькой карты.
|
||||
<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 - это отдельное приложение, которое может получить доступ (временно) только к данным треков, которые будут показаны.
|
||||
\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>
|
||||
<string name="about_preference_title">О OpenTracks</string>
|
||||
<string name="about_description"><i>OpenTracks</i> - это приложение для отслеживания спортивных событий, которое полностью уважает вашу конфиденциальность.</string>
|
||||
<string name="about_preference_title">Об OpenTracks</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_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_content">* создание GPS-треков
|
||||
\n* использование датчика частоты пульса (только Bluetooth LE)
|
||||
\n* голосовые объявления (по времени или расстоянию)
|
||||
\n* создание маркеров (в т.ч. картинки)
|
||||
\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>
|
||||
<string name="help_data_google_play_services_title">Требуется/используется ли OpenTracks сервисы Google Play\?</string>
|
||||
<string name="help_data_google_play_services_title">Требует/использует ли OpenTracks сервисы Google Play\?</string>
|
||||
<string name="help_recording_title">Как OpenTracks записывает данные\?</string>
|
||||
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> предоставляется как KMZ. </string>
|
||||
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> в формате KMZ. </string>
|
||||
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string>
|
||||
<string name="export_progress_summary_new_msg">Новые экспортируемые треки</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_without_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="generic_click_twice_cancel">Нажмите назад, чтобы отменить</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>
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="generic_skip">Пропустить</string>
|
||||
<string name="generic_open_track">Открытая трасса</string>
|
||||
<string name="generic_open_track">Открыть трек</string>
|
||||
<string name="export_progress_summary_errors_msg">Ошибки</string>
|
||||
<string name="import_file_imported">Файл %1$s импортирован</string>
|
||||
<string name="import_error_list_dialog_title">Файлы не импортированы</string>
|
||||
@@ -405,7 +405,7 @@ limitations under the License.
|
||||
<string name="open_track_as_KML">Открыть как KML</string>
|
||||
<string name="help_support_title">Как поддержать OpenTracks\?</string>
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks не регистрирует местоположения\?</string>
|
||||
<string name="activity_type_escooter">эскулап</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>
|
||||
@@ -420,33 +420,33 @@ limitations under the License.
|
||||
<string name="bluetooth_disabled">Включите Bluetooth.</string>
|
||||
<string name="sensor_state_power_avg">Средняя мощность</string>
|
||||
<string name="track_recording_notification_accuracy">Точность расположения: %1$s</string>
|
||||
<string name="stats_split_interval">Раздельный интервал</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_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="instant_export_enabled_summary">Экспорт дорожки в хранилище после завершения записи</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_gps_troubleshooting_content"> Что вы можете сделать:
|
||||
<string name="help_data_storage_content"> Данные датчиков сохраняются для каждого местоположения. Для датчика пульса это означает, что для местоположения сохраняется только <i>одно</i> значение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записываться (но будут отображаться). </string>
|
||||
<string name="help_gps_troubleshooting_content"> Что можно сделать:
|
||||
\n a) проверить, включен ли GPS,
|
||||
\n б) улучшить прием GPS (например, выехать на природу),
|
||||
\n б) улучшить прием GPS (например, выйти из помещения),
|
||||
\n c) проверить с помощью приложения <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, и
|
||||
\n г) отключите энергосбережение.
|
||||
\n г) отключть энергосбережение.
|
||||
\n
|
||||
\n Если все это не поможет, пожалуйста, сообщите об ошибке. </string>
|
||||
<string name="track_pace_notification">Темп: %1$s</string>
|
||||
<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="hold_to_stop">Удерживайте для остановки</string>
|
||||
<string name="share_image_body">Я думаю, вас может заинтересовать это изображение.</string>
|
||||
<string name="help_support_content"> Вы можете:
|
||||
\n a) распространять информацию об OpenTracks,
|
||||
@@ -463,12 +463,11 @@ limitations under the License.
|
||||
<string name="sensor_could_not_scan">Не удалось выполнить сканирование устройств Bluetooth (ошибка %1$i).</string>
|
||||
<string name="import_progress_summary_errors_msg">Ошибки</string>
|
||||
<string name="generic_overwrite">Перезаписать</string>
|
||||
<string name="import_prevent_reimport">Вы настроили предотвращение повторного импорта трека</string>
|
||||
<string name="import_prevent_reimport">Вы запретили повторный импорт трека</string>
|
||||
<string name="delete_service">Услуга удаления</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="about_third_party_data">Сторонние данные</string>
|
||||
<string name="menu_voice_rate">Скорость голоса</string>
|
||||
<string name="track_delete_not_recording">Эта дорожка не может быть удалена, поскольку она записана в данный момент.</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>
|
||||
@@ -478,7 +477,7 @@ limitations under the License.
|
||||
<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="stats_sensors_cadence">Каденс</string>
|
||||
<string name="activity_type_swimming">Плавание</string>
|
||||
<string name="activity_type_swimming_open">Плавание на открытой воде</string>
|
||||
<string name="activity_type_workout">Тренировка</string>
|
||||
@@ -523,7 +522,7 @@ limitations under the License.
|
||||
</plurals>
|
||||
<string name="settings_layout_reset">Сбросить свои раскладки</string>
|
||||
<string name="settings_layout_reset_done">Настройки всех раскладок были сброшены</string>
|
||||
<string name="generic_to">До</string>
|
||||
<string name="generic_to">по</string>
|
||||
<string name="generic_today">Сегодня</string>
|
||||
<string name="generic_yesterday">Вчера</string>
|
||||
<string name="settings_running_sensor">Бег</string>
|
||||
@@ -537,7 +536,7 @@ limitations under the License.
|
||||
<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="stats_sensors_power">Мощность</string>
|
||||
<string name="value_float_mile_hour">%1$.1f миль/ч</string>
|
||||
<string name="voice_per_kilometer">за километр</string>
|
||||
<string name="voice_per_mile">за милю</string>
|
||||
@@ -555,7 +554,7 @@ limitations under the License.
|
||||
<string name="average_heart_rate">Средний пульс</string>
|
||||
<string name="current_heart_rate">Текущий пульс</string>
|
||||
<string name="generic_filter">Фильтр</string>
|
||||
<string name="generic_from">От</string>
|
||||
<string name="generic_from">с</string>
|
||||
<string name="settings_export_section">Экспорт</string>
|
||||
<string name="settings_import_section">Импорт</string>
|
||||
<string name="settings_api_title">Публичный API</string>
|
||||
@@ -587,4 +586,33 @@ limitations under the License.
|
||||
<string name="error_crash_title">%1$s вылетел</string>
|
||||
<string name="share_crash_report">Поделиться отчётом о вылете</string>
|
||||
<string name="report_bug">Сообщить об ошибке</string>
|
||||
<plurals name="voiceSpeedMKnots">
|
||||
<item quantity="one">%1$.1f узел</item>
|
||||
<item quantity="few">%1$.1f узла</item>
|
||||
<item quantity="many">%1$.1f узлов</item>
|
||||
<item quantity="other">%1$.1f узлов</item>
|
||||
</plurals>
|
||||
<string name="unit_knots">уз</string>
|
||||
<plurals name="voiceDistanceNauticalMiles">
|
||||
<item quantity="one">%1$.2f морская миля</item>
|
||||
<item quantity="few">%1$.2f морские мили</item>
|
||||
<item quantity="many">%1$.2f морских миль</item>
|
||||
<item quantity="other">%1$.2f морских миль</item>
|
||||
</plurals>
|
||||
<string name="value_integer_knots">%1$d узлов</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="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="show_on_map_format_ask">Спрашивать</string>
|
||||
<string name="always">Запомнить</string>
|
||||
<string name="voice_per_nautical_mile">за морскую милю</string>
|
||||
<string name="settings_announcements_average_speed_pace">Средняя скорость/темп</string>
|
||||
</resources>
|
||||
@@ -517,7 +517,6 @@ limitations under the License.
|
||||
<string name="settings_sensors_title">Senzory Bluetooth</string>
|
||||
<string name="settings_sensors_summary">Cyklistické snímače, obvod kolies, snímače behu</string>
|
||||
<string name="settings_open_tracks_summary">Viac o projekte OpenTracks</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="settings_gps_summary">Interval času, interval vzdialenosti, Max. vzdialenosť, Presnosť</string>
|
||||
<string name="help_support_content"/>
|
||||
<string name="share_image_body">Myslím, že by vás mohol zaujímať tento obrázok.</string>
|
||||
|
||||
@@ -488,7 +488,6 @@ limitations under the License.
|
||||
<string name="lap_time">Čas kroga</string>
|
||||
<string name="track_detail_intervals_tab">Intervali</string>
|
||||
<string name="track_delete_not_recording">Te sledi ni mogoče izbrisati, saj je trenutno posneta.</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="stats_clock">Ura</string>
|
||||
<string name="settings_import_section">Uvoz</string>
|
||||
<string name="share_image_subject">Rad bi z vami delil sliko</string>
|
||||
|
||||
@@ -431,7 +431,6 @@ limitations under the License.
|
||||
<string name="generic_overwrite">Skriva</string>
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="export_track_errors">Spår som inte exporteras</string>
|
||||
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||
<string name="help_data_storage_content"> Sensordata lagras per inspelad plats. När det gäller en pulssensor betyder det att endast <i>en</i> pulsmätning lagras per plats. Om du står stilla eller är inomhus, kommer inga sensordata att registreras (men det kommer att visas). </string>
|
||||
<string name="help_recording_title">Hur registrerar OpenTracks data\?</string>
|
||||
<string name="about_description"><i>OpenTracks</i> är ett program för sportspårning som helt respekterar din integritet.</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user