forked from upstream-mirrors/OpenTracks
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57c0750980 | |||
| 8e36ee2e7d | |||
| ec2e527fd1 | |||
| d968e5d0c1 | |||
| 4257f21cae | |||
| 26e7059af2 | |||
| 94595f2a1f | |||
| 1fa76301e2 | |||
| ce2c608cc9 | |||
| 916f26dfda | |||
| a108edc125 | |||
| 1a1a8a851b | |||
| ddd1efeb58 | |||
| 6c30dd8509 | |||
| 21fe59f32c | |||
| f79b2b51c8 | |||
| b16e9ad964 | |||
| 65f8ba16b3 | |||
| 9b48993cd0 | |||
| 54cec3e821 | |||
| daeb972c8a | |||
| 73872e8926 | |||
| c15fa21a56 | |||
| e9d1080e5d | |||
| d0b3f45151 | |||
| 32b0b02d28 | |||
| d6617f61b7 | |||
| b5180f235f | |||
| e879eedbd6 | |||
| c7ca6800f7 | |||
| fed884a6ce | |||
| 02a77e5ce7 | |||
| 6f97399aeb | |||
| 1c4f0e67ab | |||
| ebed386d96 | |||
| e44f63feb7 | |||
| e72d2a750c | |||
| 81f6bbe955 | |||
| c162c0a0ff | |||
| 385d05dcdb | |||
| 308ce4ac07 | |||
| f8a0241e1f | |||
| c74a5c8749 | |||
| defdd49a65 |
@@ -0,0 +1,32 @@
|
||||
name: Android Test
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Android Lint
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Lint
|
||||
shell: bash
|
||||
run: ./gradlew lint
|
||||
|
||||
android-check:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'ubuntu:android-24.04'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: ./gradlew testNightlyDebugUnitTest --stacktrace
|
||||
@@ -1,10 +0,0 @@
|
||||
steps:
|
||||
build:
|
||||
when:
|
||||
event: [ push ]
|
||||
image: androidsdk/android-31
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y openjdk-17-jdk-headless
|
||||
- update-java-alternatives -a
|
||||
- ./gradlew lint
|
||||
@@ -1,10 +0,0 @@
|
||||
steps:
|
||||
build:
|
||||
when:
|
||||
event: [ push ]
|
||||
image: androidsdk/android-31
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y openjdk-17-jdk-headless
|
||||
- update-java-alternatives -a
|
||||
- ./gradlew testNightlyDebugUnitTest --stacktrace
|
||||
+3
-2
@@ -46,8 +46,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'de.dennisguse.opentracks'
|
||||
versionCode 6294
|
||||
versionName "v4.22.2"
|
||||
versionCode 6338
|
||||
versionName "v4.23.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -152,6 +152,7 @@ dependencies {
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.mediarouter:mediarouter:1.8.1'
|
||||
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
|
||||
implementation 'androidx.work:work-runtime:2.10.5'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.7.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
v4.23.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- UI: added average pace of current segment
|
||||
(currently only available in new layouts)
|
||||
- Clicking on notification opens actual recording UI
|
||||
|
||||
Developer:
|
||||
- Deprecated JobIntentService replaced with WorkManager
|
||||
- Use .isEmpty() except for CharSequence and Stream.toList()
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.22.0: OpenTracks
|
||||
|
||||
Modifiche:
|
||||
- Migrazione a https://codeberg.org/OpenTracksApp/OpenTracks
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.22.1: OpenTracks
|
||||
|
||||
Modiciche:
|
||||
- Primo rilascio da Codeberg
|
||||
@@ -0,0 +1,4 @@
|
||||
v4.22.2: OpenTracks
|
||||
|
||||
Correzione di errori:
|
||||
- L'impostazione degli annunci vocali per l'ora e l'unità di misura è stata collegata
|
||||
@@ -0,0 +1,10 @@
|
||||
(versionName): OpenTracks
|
||||
|
||||
Modifiche:
|
||||
-
|
||||
|
||||
Correzioni di errori:
|
||||
-
|
||||
|
||||
Sviluppatore:
|
||||
-
|
||||
@@ -1,22 +1,22 @@
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
尊重您隐私的运动追踪伙伴。
|
||||
|
||||
健康幸福的生活总少不了各种运动和户外活动。
|
||||
运动与户外活动能带来快乐、提升技能、增强自信。
|
||||
|
||||
使用本应用,您可以保持对您的训练数据的有效追踪,为您的健康助力。
|
||||
本应用可以在您跑步或散步时记录数据,也可以在您骑车时作为一个大屏幕自行车电脑。
|
||||
您可以为路上的有趣位置创建标记,还可以用图片作为标记。
|
||||
您的数据会被详尽地记录下来,供您查阅分析。
|
||||
您的数据只会分享给您想要分享给的人。
|
||||
通过跟踪训练数据,珍视您的健康。
|
||||
记录您的跑步或步行数据,并提供大屏的专用骑行码表。
|
||||
用照片标记沿途有趣的地点。
|
||||
保留详细记录的统计数据以供分析。
|
||||
仅分享您愿意公开的数据。
|
||||
|
||||
* 语音播报
|
||||
* 支持蓝牙LE传感器:心率、速度和距离(骑车)、踏频(骑车)和功率计(骑车)。
|
||||
* 海拔增减:通过气压传感器。
|
||||
* EGM2008 海拔显示(平均海平面以上);导出为 WGS84。
|
||||
* 导出路线数据,支持 KMZ(包括照片)、KML 和 GPX 格式。
|
||||
* 不需要联网,不需要额外权限。
|
||||
* 语音播报。
|
||||
* 支持蓝牙低功耗传感器:心率、速度和距离(骑行)、踏频(骑行)、功率计(骑行)。
|
||||
* 海拔上升和下降:通过气压传感器。
|
||||
* 采用 EGM2008 显示海拔(平均海平面以上);导出为 WGS84。
|
||||
* 以 KMZ(含照片)、KML 或 GPX 格式导出轨迹数据。
|
||||
* 无需互联网访问或额外权限。
|
||||
* 深色和浅色主题,跟随系统设置。
|
||||
* 没有广告。
|
||||
* 无广告。
|
||||
|
||||
本应用是开源软件
|
||||
意味着您可以使用、学习、修改和分发本应用的源代码。
|
||||
本应用使用 Apache 2.0 协议发布。
|
||||
自由软件/开放源代码
|
||||
意味着您可以使用、研究、修改和分享源代码。
|
||||
采用 Apache 2.0 许可
|
||||
|
||||
@@ -1 +1 @@
|
||||
一款尊重您的隐私的运动追踪应用。
|
||||
尊重您隐私的运动追踪伙伴。
|
||||
|
||||
+4
-5
@@ -47,7 +47,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
|
||||
@@ -451,7 +450,7 @@ public class CustomContentProviderUtilsTest {
|
||||
contentProviderUtils.insertMarker(marker1);
|
||||
|
||||
// Check insert was done.
|
||||
assertEquals(contentProviderUtils.getMarkers(trackId).size(), 1);
|
||||
assertEquals(1, contentProviderUtils.getMarkers(trackId).size());
|
||||
|
||||
// Get marker id that needs to delete.
|
||||
Marker.Id marker1Id = contentProviderUtils.insertMarker(marker1);
|
||||
@@ -478,7 +477,7 @@ public class CustomContentProviderUtilsTest {
|
||||
contentProviderUtils.insertMarker(marker1);
|
||||
|
||||
// Check insert was done.
|
||||
assertEquals(contentProviderUtils.getMarkers(trackId).size(), 1);
|
||||
assertEquals(1, contentProviderUtils.getMarkers(trackId).size());
|
||||
|
||||
// Get marker id that needs to delete.
|
||||
Marker.Id marker1Id = contentProviderUtils.insertMarker(marker1);
|
||||
@@ -832,7 +831,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
List<TrackPoint.Id> trackpointIds = track.second.stream()
|
||||
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
|
||||
.map(TrackPoint.Id::new).collect(Collectors.toList());
|
||||
.map(TrackPoint.Id::new).toList();
|
||||
|
||||
// when
|
||||
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds.get(8))) {
|
||||
@@ -850,7 +849,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
List<TrackPoint.Id> trackpointIds = track.second.stream()
|
||||
.map(it -> ContentUris.parseId(contentProviderUtils.insertTrackPoint(it, track.first.getId())))
|
||||
.map(TrackPoint.Id::new).collect(Collectors.toList());
|
||||
.map(TrackPoint.Id::new).toList();
|
||||
|
||||
TrackPoint.Id startTrackPointId = trackpointIds.get(0);
|
||||
|
||||
|
||||
@@ -48,10 +48,8 @@ public class CustomSQLiteOpenHelperTest {
|
||||
public static Map<String, String> getSQL(SQLiteDatabase db, String type) {
|
||||
HashMap<String, String> sqlMap = new HashMap<>();
|
||||
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "type=?", new String[]{type}, null, null, "name")) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
sqlMap.put(cursor.getString(0), cursor.getString(1));
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
sqlMap.put(cursor.getString(0), cursor.getString(1));
|
||||
}
|
||||
}
|
||||
return sqlMap;
|
||||
@@ -64,12 +62,10 @@ public class CustomSQLiteOpenHelperTest {
|
||||
*/
|
||||
private static boolean hasSqlCreate(SQLiteDatabase db, String sqlCreate) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT SQL FROM sqlite_master", null)) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String sql = cursor.getString(0);
|
||||
if (sqlCreate.equals(sql)) {
|
||||
return true;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
String sql = cursor.getString(0);
|
||||
if (sqlCreate.equals(sql)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "_id IN (?)");
|
||||
assertEquals(selection.selectionArgs().length, 1);
|
||||
assertEquals(selection.selectionArgs()[0], "1");
|
||||
assertEquals("_id IN (?)", selection.selection());
|
||||
assertEquals(1, selection.selectionArgs().length);
|
||||
assertEquals("1", selection.selectionArgs()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,11 +57,11 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "_id IN (?,?,?)");
|
||||
assertEquals(selection.selectionArgs().length, 3);
|
||||
assertEquals(selection.selectionArgs()[0], "1");
|
||||
assertEquals(selection.selectionArgs()[1], "2");
|
||||
assertEquals(selection.selectionArgs()[2], "3");
|
||||
assertEquals("_id IN (?,?,?)", selection.selection());
|
||||
assertEquals(3, selection.selectionArgs().length);
|
||||
assertEquals("1", selection.selectionArgs()[0]);
|
||||
assertEquals("2", selection.selectionArgs()[1]);
|
||||
assertEquals("3", selection.selectionArgs()[2]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,9 +73,9 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "category IN (?)");
|
||||
assertEquals(selection.selectionArgs().length, 1);
|
||||
assertEquals(selection.selectionArgs()[0], "running");
|
||||
assertEquals("category IN (?)", selection.selection());
|
||||
assertEquals(1, selection.selectionArgs().length);
|
||||
assertEquals("running", selection.selectionArgs()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,12 +91,12 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "category IN (?,?,?,?)");
|
||||
assertEquals(selection.selectionArgs().length, 4);
|
||||
assertEquals(selection.selectionArgs()[0], "running");
|
||||
assertEquals(selection.selectionArgs()[1], "road biking");
|
||||
assertEquals(selection.selectionArgs()[2], "mountain biking");
|
||||
assertEquals(selection.selectionArgs()[3], "trail walking");
|
||||
assertEquals("category IN (?,?,?,?)", selection.selection());
|
||||
assertEquals(4, selection.selectionArgs().length);
|
||||
assertEquals("running", selection.selectionArgs()[0]);
|
||||
assertEquals("road biking", selection.selectionArgs()[1]);
|
||||
assertEquals("mountain biking", selection.selectionArgs()[2]);
|
||||
assertEquals("trail walking", selection.selectionArgs()[3]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,8 +120,8 @@ public class TrackSelectionTest extends TestCase {
|
||||
assertNull(selectionWrong2.selection());
|
||||
assertNull(selectionWrong2.selectionArgs());
|
||||
|
||||
assertEquals(selectionOk.selection(), "starttime BETWEEN ? AND ?");
|
||||
assertEquals(selectionOk.selectionArgs().length, 2);
|
||||
assertEquals("starttime BETWEEN ? AND ?", selectionOk.selection());
|
||||
assertEquals(2, selectionOk.selectionArgs().length);
|
||||
assertEquals(selectionOk.selectionArgs()[0], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selectionOk.selectionArgs()[1], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
@@ -143,13 +143,13 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "_id IN (?,?,?) AND category IN (?,?)");
|
||||
assertEquals(selection.selectionArgs().length, 5);
|
||||
assertEquals(selection.selectionArgs()[0], "1");
|
||||
assertEquals(selection.selectionArgs()[1], "2");
|
||||
assertEquals(selection.selectionArgs()[2], "3");
|
||||
assertEquals(selection.selectionArgs()[3], "running");
|
||||
assertEquals(selection.selectionArgs()[4], "road biking");
|
||||
assertEquals("_id IN (?,?,?) AND category IN (?,?)", selection.selection());
|
||||
assertEquals(5, selection.selectionArgs().length);
|
||||
assertEquals("1", selection.selectionArgs()[0]);
|
||||
assertEquals("2", selection.selectionArgs()[1]);
|
||||
assertEquals("3", selection.selectionArgs()[2]);
|
||||
assertEquals("running", selection.selectionArgs()[3]);
|
||||
assertEquals("road biking", selection.selectionArgs()[4]);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -172,11 +172,11 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "_id IN (?,?,?) AND starttime BETWEEN ? AND ?");
|
||||
assertEquals(selection.selectionArgs().length, 5);
|
||||
assertEquals(selection.selectionArgs()[0], "1");
|
||||
assertEquals(selection.selectionArgs()[1], "2");
|
||||
assertEquals(selection.selectionArgs()[2], "3");
|
||||
assertEquals("_id IN (?,?,?) AND starttime BETWEEN ? AND ?", selection.selection());
|
||||
assertEquals(5, selection.selectionArgs().length);
|
||||
assertEquals("1", selection.selectionArgs()[0]);
|
||||
assertEquals("2", selection.selectionArgs()[1]);
|
||||
assertEquals("3", selection.selectionArgs()[2]);
|
||||
assertEquals(selection.selectionArgs()[3], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selection.selectionArgs()[4], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
@@ -196,10 +196,10 @@ public class TrackSelectionTest extends TestCase {
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.selection(), "category IN (?,?) AND starttime BETWEEN ? AND ?");
|
||||
assertEquals(selection.selectionArgs().length, 4);
|
||||
assertEquals(selection.selectionArgs()[0], "running");
|
||||
assertEquals(selection.selectionArgs()[1], "road biking");
|
||||
assertEquals("category IN (?,?) AND starttime BETWEEN ? AND ?", selection.selection());
|
||||
assertEquals(4, selection.selectionArgs().length);
|
||||
assertEquals("running", selection.selectionArgs()[0]);
|
||||
assertEquals("road biking", selection.selectionArgs()[1]);
|
||||
assertEquals(selection.selectionArgs()[2], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selection.selectionArgs()[3], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@ public class DistanceFormatterTest {
|
||||
@Test
|
||||
public void testFormatDistance_metric() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setDecimalPlaces(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.build(context);
|
||||
|
||||
@@ -34,7 +34,7 @@ public class DistanceFormatterTest {
|
||||
@Test
|
||||
public void testFormatDistance_imperial() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setDecimalPlaces(2)
|
||||
.setUnit(UnitSystem.IMPERIAL_FEET)
|
||||
.build(context);
|
||||
|
||||
@@ -47,7 +47,7 @@ public class DistanceFormatterTest {
|
||||
@Test
|
||||
public void testFormatDistance_nautical() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setDecimalPlaces(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.build(context);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_metric() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
@@ -35,7 +34,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_imperial() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.IMPERIAL_FEET)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
@@ -47,7 +45,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_nautical() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
@@ -59,7 +56,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_metric() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
@@ -71,7 +67,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_imperial() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.IMPERIAL_FEET)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
@@ -83,7 +78,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_nautical() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.NAUTICAL_IMPERIAL)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
@@ -95,7 +89,6 @@ public class SpeedFormatterTest {
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class UintUtilsTest {
|
||||
|
||||
@Test
|
||||
public void diff() {
|
||||
public void diff_uint16_valid() {
|
||||
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT16_MAX));
|
||||
assertEquals(65535, UintUtils.diff(1, 2, UintUtils.UINT16_MAX)); /* unsigned 16 arithmetic is modulo UINT16_MAX + 1, not modulo UINT16_MAX */
|
||||
assertEquals(65530, UintUtils.diff(UintUtils.UINT16_MAX, 5, UintUtils.UINT16_MAX));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diff_uint16_invalid() {
|
||||
/* Test modulo arithmetic for arguments that are out of range */
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, 1, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, 65537, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, 65537, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, 1, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, -65535, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, -65535, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-65535, 65537, UintUtils.UINT16_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, -65535, UintUtils.UINT16_MAX));
|
||||
}
|
||||
|
||||
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
|
||||
assertEquals(0, UintUtils.diff(65537, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(1, 65537, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(65537, 65537, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(-65535, 1, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(1, -65535, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(-65535, -65535, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(-65535, 65537, UintUtils.UINT16_MAX));
|
||||
assertEquals(0, UintUtils.diff(65537, -65535, UintUtils.UINT16_MAX));
|
||||
}
|
||||
|
||||
/* The following tests are the above, but for 32-bit unsigned. */
|
||||
|
||||
@Test
|
||||
public void diff_uint32() {
|
||||
assertEquals(0, UintUtils.diff(1, 1, UintUtils.UINT32_MAX));
|
||||
assertEquals(1, UintUtils.diff(2, 1, UintUtils.UINT32_MAX));
|
||||
assertEquals(3, UintUtils.diff(5, 2, UintUtils.UINT32_MAX));
|
||||
assertEquals(4294967295L, UintUtils.diff(1, 2, UintUtils.UINT32_MAX));
|
||||
assertEquals(4294967290L, UintUtils.diff(UintUtils.UINT32_MAX, 5, UintUtils.UINT32_MAX));
|
||||
}
|
||||
|
||||
if (false) { /* false means UintUtils.diff() throws if arguments out of range */
|
||||
assertEquals(0, UintUtils.diff(4294967297L, 1, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(1, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(4294967297L, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(-4294967295L, 1, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(1, -4294967295L, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(-4294967295L, -4294967295L, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(-4294967295L, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertEquals(0, UintUtils.diff(65537, -4294967297L, UintUtils.UINT32_MAX));
|
||||
}
|
||||
@Test
|
||||
public void diff_unint32_invalid() {
|
||||
/* Test modulo arithmetic for arguments that are out of range */
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(4294967297L, 1, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(4294967297L, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, 1, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(1, -4294967295L, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, -4294967295L, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(-4294967295L, 4294967297L, UintUtils.UINT32_MAX));
|
||||
assertThrows(RuntimeException.class, () -> UintUtils.diff(65537, -4294967297L, UintUtils.UINT32_MAX));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-4
@@ -18,6 +18,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
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.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@@ -51,11 +52,15 @@ public class TrackRecordingServiceNotificationManagerTest {
|
||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
|
||||
subject.setUnitSystem(UnitSystem.METRIC);
|
||||
|
||||
Track track = new Track();
|
||||
track.setTrackStatistics(trackStatisticsMock);
|
||||
RecordingData data = new RecordingData(track, trackPointMock, null, null);
|
||||
|
||||
// when
|
||||
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
|
||||
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
|
||||
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(1000));
|
||||
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
subject.updateTrackPoint(context, data, Distance.of(1000));
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
|
||||
// then
|
||||
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
|
||||
|
||||
+6
-6
@@ -165,7 +165,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
@@ -216,7 +216,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
@@ -330,7 +330,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
@@ -381,7 +381,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
@@ -411,7 +411,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
@@ -449,7 +449,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
TrackStatistics stats = trackWithStats.second;
|
||||
IntervalStatistics.Interval lastInterval;
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
|
||||
assertEquals(trackPointIterator.getCount(), numberOfPoints);
|
||||
assertEquals(numberOfPoints, trackPointIterator.getCount());
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
|
||||
|
||||
+5
-5
@@ -41,7 +41,7 @@ public class PreferencesOpenHelperTest {
|
||||
// then there should be one layout with old custom layout that has the new CSV value.
|
||||
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
|
||||
assertNotNull(recordingLayouts);
|
||||
assertEquals(recordingLayouts.size(), 1);
|
||||
assertEquals(1, recordingLayouts.size());
|
||||
assertEquals(recordingLayouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv());
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class PreferencesOpenHelperTest {
|
||||
// then there should be one layout with old custom layout that has the new CSV value.
|
||||
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
|
||||
assertNotNull(recordingLayouts);
|
||||
assertEquals(recordingLayouts.size(), 1);
|
||||
assertEquals(1, recordingLayouts.size());
|
||||
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ public class PreferencesOpenHelperTest {
|
||||
// then there should be one layout with old custom layout that has the new CSV value.
|
||||
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
|
||||
assertNotNull(recordingLayouts);
|
||||
assertEquals(recordingLayouts.size(), 1);
|
||||
assertEquals(recordingLayouts.get(0).toCsv(), updatedOldCustomLayoutCsv);
|
||||
assertEquals(1, recordingLayouts.size());
|
||||
assertEquals(updatedOldCustomLayoutCsv, recordingLayouts.get(0).toCsv());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,7 +113,7 @@ public class PreferencesOpenHelperTest {
|
||||
RecordingLayout defaultRecordingLayout = RecordingLayoutIO.fromCsv(PreferencesUtils.buildDefaultLayout(), resources);
|
||||
List<RecordingLayout> customRecordingLayout = PreferencesUtils.getAllCustomLayouts();
|
||||
|
||||
assertEquals(customRecordingLayout.size(), 1);
|
||||
assertEquals(1, customRecordingLayout.size());
|
||||
assertEquals(defaultRecordingLayout.toCsv(), customRecordingLayout.get(0).toCsv());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ public class PreferencesUtilsTest {
|
||||
List<RecordingLayout> recordingLayouts = PreferencesUtils.getAllCustomLayouts();
|
||||
|
||||
// then
|
||||
assertEquals(recordingLayouts.size(), 1);
|
||||
assertTrue(recordingLayouts.get(0).getFields().size() > 0);
|
||||
assertEquals(1, recordingLayouts.size());
|
||||
assertFalse(recordingLayouts.get(0).getFields().isEmpty());
|
||||
assertEquals(recordingLayouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout));
|
||||
assertTrue(recordingLayouts.get(0).getFields().stream().anyMatch(DataField::isVisible));
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class PreferencesUtilsTest {
|
||||
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
|
||||
|
||||
// then
|
||||
assertTrue(recordingLayout.getFields().size() > 0);
|
||||
assertFalse(recordingLayout.getFields().isEmpty());
|
||||
assertEquals(recordingLayout.getName(), context.getString(R.string.stats_custom_layout_default_layout));
|
||||
assertTrue(recordingLayout.getFields().stream().anyMatch(DataField::isVisible));
|
||||
}
|
||||
@@ -130,9 +130,9 @@ public class PreferencesUtilsTest {
|
||||
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
|
||||
|
||||
// then
|
||||
assertEquals(recordingLayout.getFields().size(), 4);
|
||||
assertEquals(recordingLayout.getName(), "run");
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), 2);
|
||||
assertEquals(4, recordingLayout.getFields().size());
|
||||
assertEquals("run", recordingLayout.getName());
|
||||
assertEquals(2, recordingLayout.getColumnsPerRow());
|
||||
|
||||
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
|
||||
assertTrue(recordingLayout.getFields().get(0).isVisible());
|
||||
@@ -169,9 +169,9 @@ public class PreferencesUtilsTest {
|
||||
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
|
||||
|
||||
// then
|
||||
assertEquals(recordingLayout.getFields().size(), 4);
|
||||
assertEquals(recordingLayout.getName(), "walking");
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), 2);
|
||||
assertEquals(4, recordingLayout.getFields().size());
|
||||
assertEquals("walking", recordingLayout.getName());
|
||||
assertEquals(2, recordingLayout.getColumnsPerRow());
|
||||
|
||||
assertEquals(recordingLayout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key));
|
||||
assertTrue(recordingLayout.getFields().get(0).isVisible());
|
||||
@@ -266,11 +266,11 @@ public class PreferencesUtilsTest {
|
||||
// then only updated profile is modified in the custom layouts
|
||||
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
|
||||
|
||||
assertEquals(layoutsBefore.size(), 2);
|
||||
assertEquals(layoutsAfter.size(), 2);
|
||||
assertEquals(2, layoutsBefore.size());
|
||||
assertEquals(2, layoutsAfter.size());
|
||||
|
||||
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
|
||||
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
|
||||
assertEquals(4, layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count());
|
||||
assertEquals(1, layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -309,11 +309,11 @@ public class PreferencesUtilsTest {
|
||||
// then only updated profile is modified in the custom layouts
|
||||
List<RecordingLayout> layoutsAfter = PreferencesUtils.getAllCustomLayouts();
|
||||
|
||||
assertEquals(layoutsBefore.size(), 2);
|
||||
assertEquals(layoutsAfter.size(), 2);
|
||||
assertEquals(2, layoutsBefore.size());
|
||||
assertEquals(2, layoutsAfter.size());
|
||||
|
||||
assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4);
|
||||
assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1);
|
||||
assertEquals(4, layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count());
|
||||
assertEquals(1, layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -341,6 +341,6 @@ public class PreferencesUtilsTest {
|
||||
RecordingLayout recordingLayout = PreferencesUtils.getCustomLayout();
|
||||
|
||||
// then the first one was returned
|
||||
assertEquals(recordingLayout.getName(), "cycling");
|
||||
assertEquals("cycling", recordingLayout.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 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.share;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link DescriptionGenerator}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DescriptionGeneratorTest {
|
||||
|
||||
@Rule
|
||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||
|
||||
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
|
||||
private DescriptionGenerator descriptionGenerator;
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
descriptionGenerator = new DescriptionGenerator(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#generateTrackDescription(Track, boolean)}.
|
||||
*/
|
||||
@Test
|
||||
public void testGenerateTrackDescription() {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setMaxAltitude(550);
|
||||
stats.setMinAltitude(-500);
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
stats.setTotalAltitudeLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setActivityTypeLocalized("hiking");
|
||||
String expected = //"Created by"
|
||||
"<a href='https://codeberg.org/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
|
||||
+ "Name: -<br>"
|
||||
+ "Activity type: hiking<br>"
|
||||
+ "Description: -<br>"
|
||||
+ "Total distance: 20.00 km (12.4 mi)<br>"
|
||||
+ "Total time: 10:00<br>"
|
||||
+ "Moving time: 05:00<br>"
|
||||
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>"
|
||||
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
|
||||
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>"
|
||||
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>"
|
||||
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
|
||||
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
|
||||
+ "Max elevation: 550 m (1804 ft)<br>"
|
||||
+ "Min elevation: -500 m (-1640 ft)<br>"
|
||||
+ "Elevation gain: 6000 m (19685 ft)<br>"
|
||||
+ "Elevation loss: 6000 m (19685 ft)<br>"
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateTrackDescriptionWithoutMaxMinAltitude() {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setMaxAltitude(Double.POSITIVE_INFINITY);
|
||||
stats.setMinAltitude(Double.NEGATIVE_INFINITY);
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
stats.setTotalAltitudeLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setActivityTypeLocalized("hiking");
|
||||
String expected = //"Created by"
|
||||
"<a href='https://codeberg.org/OpenTracksApp/OpenTracks'>OpenTracks (Debug)</a><p>"
|
||||
+ "Name: -<br>"
|
||||
+ "Activity type: hiking<br>"
|
||||
+ "Description: -<br>"
|
||||
+ "Total distance: 20.00 km (12.4 mi)<br>"
|
||||
+ "Total time: 10:00<br>"
|
||||
+ "Moving time: 05:00<br>"
|
||||
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>"
|
||||
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
|
||||
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>"
|
||||
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>"
|
||||
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
|
||||
+ "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.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "<br>";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeDistance(Distance, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteDistance() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance, "<br>");
|
||||
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeTime(Duration, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteTime() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time, "<br>");
|
||||
assertEquals("Total time: 00:01<br>", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeSpeed(Speed, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteSpeed() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed, "\n");
|
||||
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writeAltitude(double, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWriteAltitude() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeAltitude(4.2, builder, R.string.description_min_altitude, "<br>");
|
||||
assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DescriptionGenerator#writePace(Speed, StringBuilder, int, String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testWritePace() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute, "\n");
|
||||
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2012 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.share;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link TrackShareSummaryGenerator}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackShareSummaryGeneratorTest {
|
||||
|
||||
@Rule
|
||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||
|
||||
private static final Instant START_TIME = Instant.ofEpochMilli(1288721514000L);
|
||||
private TrackShareSummaryGenerator descriptionGenerator;
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
descriptionGenerator = new TrackShareSummaryGenerator(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateTrackDescription() {
|
||||
Track track = new Track();
|
||||
TrackStatistics stats = new TrackStatistics();
|
||||
stats.setTotalDistance(Distance.of(20000));
|
||||
stats.setTotalTime(Duration.ofMillis(600000));
|
||||
stats.setMovingTime(Duration.ofMillis(300000));
|
||||
stats.setMaxSpeed(Speed.of(100));
|
||||
stats.setMaxAltitude(550);
|
||||
stats.setMinAltitude(-500);
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
stats.setTotalAltitudeLoss(6000f);
|
||||
stats.setStartTime(START_TIME);
|
||||
track.setTrackStatistics(stats);
|
||||
track.setActivityTypeLocalized("hiking");
|
||||
String expected = //"Created by"
|
||||
"OpenTracks (Debug)\n\n"
|
||||
+ "Name: -\n"
|
||||
+ "Activity type: hiking\n"
|
||||
+ "Description: -\n"
|
||||
+ "Total distance: 20.00 km (12.4 mi)\n"
|
||||
+ "Total time: 10:00\n"
|
||||
+ "Moving time: 05:00\n"
|
||||
+ "Average speed: 120.00 km/h (74.6 mi/h)\n"
|
||||
+ "Average moving speed: 240.00 km/h (149.1 mi/h)\n"
|
||||
+ "Max speed: 360.00 km/h (223.7 mi/h)\n"
|
||||
+ "Average pace: 0:30 min/km (0:48 min/mi)\n"
|
||||
+ "Average moving pace: 0:15 min/km (0:24 min/mi)\n"
|
||||
+ "Fastest pace: 0:10 min/km (0:16 min/mi)\n"
|
||||
+ "Max elevation: 550 m (1804 ft)\n"
|
||||
+ "Min elevation: -500 m (-1640 ft)\n"
|
||||
+ "Elevation gain: 6000 m (19685 ft)\n"
|
||||
+ "Elevation loss: 6000 m (19685 ft)\n"
|
||||
+ "Recorded: " + StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(START_TIME, ZoneId.systemDefault())) + "\n";
|
||||
|
||||
assertEquals(expected, descriptionGenerator.generateTrackDescription(track));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteDistance() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance);
|
||||
assertEquals("Total distance: 1.10 km (0.7 mi)\n", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteTime() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeTime(Duration.ofMillis(1000), builder, R.string.description_total_time);
|
||||
assertEquals("Total time: 00:01\n", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteSpeed() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed);
|
||||
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteAltitude() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writeAltitude(4.2, builder, R.string.description_min_altitude);
|
||||
assertEquals("Min elevation: 4 m (14 ft)\n", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWritePace() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute);
|
||||
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -26,11 +26,11 @@ public class CsvRecordingLayoutUtilsTest extends TestCase {
|
||||
|
||||
// then
|
||||
assertNotNull(partsOk);
|
||||
assertEquals(partsOk.size(), 4);
|
||||
assertEquals(partsOk.get(0), "Layout Name");
|
||||
assertEquals(partsOk.get(1), "2");
|
||||
assertEquals(partsOk.get(2), "distance,1,1");
|
||||
assertEquals(partsOk.get(3), "speed,1,1");
|
||||
assertEquals(4, partsOk.size());
|
||||
assertEquals("Layout Name", partsOk.get(0));
|
||||
assertEquals("2", partsOk.get(1));
|
||||
assertEquals("distance,1,1", partsOk.get(2));
|
||||
assertEquals("speed,1,1", partsOk.get(3));
|
||||
assertNull(partsWrong1);
|
||||
assertNull(partsWrong2);
|
||||
}
|
||||
|
||||
+12
-12
@@ -32,9 +32,9 @@ public class RecordingLayoutTest extends TestCase {
|
||||
List<DataField> dataFieldList = recordingLayout.getFields();
|
||||
|
||||
// then layout and data fields are built correctly
|
||||
assertEquals(recordingLayout.getName(), "running");
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), 2);
|
||||
assertEquals(dataFieldList.size(), 4);
|
||||
assertEquals("running", recordingLayout.getName());
|
||||
assertEquals(2, recordingLayout.getColumnsPerRow());
|
||||
assertEquals(4, dataFieldList.size());
|
||||
assertEquals(dataFieldList.get(0).getKey(), context.getString(R.string.stats_custom_layout_speed_key));
|
||||
assertEquals(dataFieldList.get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key));
|
||||
assertEquals(dataFieldList.get(2).getKey(), context.getString(R.string.stats_custom_layout_altitude_key));
|
||||
@@ -66,7 +66,7 @@ public class RecordingLayoutTest extends TestCase {
|
||||
// then layout and data fields are built correctly
|
||||
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
|
||||
assertEquals(recordingLayout.getFields().size(), 0);
|
||||
assertEquals(0, recordingLayout.getFields().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -81,7 +81,7 @@ public class RecordingLayoutTest extends TestCase {
|
||||
// then layout and data fields are built correctly
|
||||
assertEquals(recordingLayout.getName(), PreferencesUtils.getDefaultLayoutName());
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault());
|
||||
assertEquals(recordingLayout.getFields().size(), 0);
|
||||
assertEquals(0, recordingLayout.getFields().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,9 +94,9 @@ public class RecordingLayoutTest extends TestCase {
|
||||
List<DataField> dataFieldList = recordingLayout.getFields();
|
||||
|
||||
// then layout and data fields are built correctly
|
||||
assertEquals(recordingLayout.getName(), "Layout Name");
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), 2);
|
||||
assertEquals(recordingLayout.getFields().size(), 0);
|
||||
assertEquals("Layout Name", recordingLayout.getName());
|
||||
assertEquals(2, recordingLayout.getColumnsPerRow());
|
||||
assertEquals(0, recordingLayout.getFields().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,9 +109,9 @@ public class RecordingLayoutTest extends TestCase {
|
||||
List<DataField> dataFieldList = recordingLayout.getFields();
|
||||
|
||||
// then layout and data fields are built correctly
|
||||
assertEquals(recordingLayout.getName(), "Layout Name");
|
||||
assertEquals(recordingLayout.getColumnsPerRow(), 2);
|
||||
assertEquals(recordingLayout.getFields().size(), 1);
|
||||
assertEquals("Layout Name", recordingLayout.getName());
|
||||
assertEquals(2, recordingLayout.getColumnsPerRow());
|
||||
assertEquals(1, recordingLayout.getFields().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,7 +128,7 @@ public class RecordingLayoutTest extends TestCase {
|
||||
String csv = recordingLayout.toCsv();
|
||||
|
||||
// then csv is well built
|
||||
assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;");
|
||||
assertEquals("Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;", csv);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class StatisticsUtilsTest extends TestCase {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void testEmptyValue() {
|
||||
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_distance_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_total_time_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_speed_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_moving_time_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_speed_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_max_speed_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_speed_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_altitude_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_gain_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_loss_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_pace_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_pace_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_pace_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_fastest_pace_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_coordinates), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_coordinates_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_heart_rate_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_cadence_key)));
|
||||
assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_power_key)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterVisible() {
|
||||
// given
|
||||
RecordingLayout recordingLayout = new RecordingLayout("profile");
|
||||
recordingLayout.addField(new DataField("key1", true, true, false));
|
||||
recordingLayout.addField(new DataField("key2", false, true, false));
|
||||
recordingLayout.addField(new DataField("key3", true, true, false));
|
||||
recordingLayout.addField(new DataField("key4", false, true, false));
|
||||
recordingLayout.addField(new DataField("key5", true, true, false));
|
||||
|
||||
// when
|
||||
RecordingLayout resultTrue = recordingLayout.toRecordingLayout(true);
|
||||
RecordingLayout resultFalse = recordingLayout.toRecordingLayout(false);
|
||||
|
||||
// then
|
||||
assertEquals(resultTrue.getFields().size(), 3);
|
||||
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key1".equals(f.getKey())));
|
||||
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key3".equals(f.getKey())));
|
||||
assertTrue(resultTrue.getFields().stream().anyMatch(f -> "key5".equals(f.getKey())));
|
||||
|
||||
assertEquals(resultFalse.getFields().size(), 2);
|
||||
assertTrue(resultFalse.getFields().stream().anyMatch(f -> "key2".equals(f.getKey())));
|
||||
assertTrue(resultFalse.getFields().stream().anyMatch(f -> "key4".equals(f.getKey())));
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,10 @@ import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.TimezoneRule;
|
||||
@@ -133,8 +132,10 @@ public class StringUtilsTest {
|
||||
@Test
|
||||
public void testFormatDateTodayRelative() {
|
||||
// given
|
||||
ArrayList<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
|
||||
ArrayList<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault())).collect(Collectors.toCollection(ArrayList::new));
|
||||
List<String> shortDays = Arrays.stream(DayOfWeek.values()).map(d -> d.getDisplayName(TextStyle.FULL, Locale.getDefault()))
|
||||
.toList();
|
||||
List<String> shortMonths = Arrays.stream(Month.values()).map(m -> m.getDisplayName(TextStyle.SHORT, Locale.getDefault()))
|
||||
.toList();
|
||||
|
||||
LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate();
|
||||
LocalDate yesterday = today.minusDays(1);
|
||||
|
||||
@@ -395,21 +395,6 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
android:label="@string/recording_service" />
|
||||
|
||||
<service
|
||||
android:name=".io.file.exporter.ExportService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".io.file.importer.ImportService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".services.TrackDeleteService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -16,15 +16,18 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.services.TrackDeleteService;
|
||||
import de.dennisguse.opentracks.services.TrackDeletionWorker;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||
|
||||
@@ -37,7 +40,7 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
|
||||
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteService.TrackDeleteResultReceiver.Receiver {
|
||||
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller {
|
||||
|
||||
protected void deleteTracks(Track.Id... trackIds) {
|
||||
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
|
||||
@@ -46,8 +49,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
||||
//TODO A callback is better.
|
||||
@Override
|
||||
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
||||
ArrayList<Track.Id> trackIdList = Arrays.stream(trackIds)
|
||||
.filter(trackId -> !trackId.equals(getRecordingTrackId())).collect(Collectors.toCollection(ArrayList::new));
|
||||
List<Track.Id> trackIdList = Arrays.stream(trackIds)
|
||||
.filter(trackId -> !trackId.equals(getRecordingTrackId())).toList();
|
||||
|
||||
onDeleteConfirmed();
|
||||
|
||||
@@ -55,10 +58,29 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
|
||||
Toast.makeText(this, getString(R.string.track_delete_not_recording), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
TrackDeleteService.enqueue(this, new TrackDeleteService.TrackDeleteResultReceiver(new Handler(), this), trackIdList);
|
||||
WorkManager workManager = WorkManager.getInstance(this);
|
||||
WorkRequest deleteRequest = new OneTimeWorkRequest.Builder(TrackDeletionWorker.class)
|
||||
.setInputData(new Data.Builder()
|
||||
.putLongArray(TrackDeletionWorker.TRACKIDS_KEY, Arrays.stream(trackIds).mapToLong(Track.Id::id).toArray())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
workManager
|
||||
.getWorkInfoByIdLiveData(deleteRequest.getId())
|
||||
.observe(this, workInfo -> {
|
||||
if (workInfo != null) {
|
||||
if (workInfo.getState().isFinished()) {
|
||||
onDeleteFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workManager.enqueue(deleteRequest);
|
||||
}
|
||||
|
||||
protected abstract void onDeleteConfirmed();
|
||||
|
||||
protected abstract void onDeleteFinished();
|
||||
|
||||
protected abstract Track.Id getRecordingTrackId();
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package de.dennisguse.opentracks.chart;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.Paint;
|
||||
@@ -49,10 +48,8 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.ExtremityMonitor;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
import de.dennisguse.opentracks.ui.util.ThemeUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -857,7 +854,7 @@ public class ChartView extends View {
|
||||
* @param chartValueSeries the chart value series
|
||||
* @param canvas the canvas
|
||||
* @param xPosition the right most x position
|
||||
* @parm yOffset offset to apply to y position
|
||||
* @param yOffset offset to apply to y position
|
||||
* @param yValue the y value
|
||||
* @return the marker width.
|
||||
*/
|
||||
@@ -883,7 +880,7 @@ public class ChartView extends View {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstChartValueSeries != null && chartPoints.size() > 0) {
|
||||
if (firstChartValueSeries != null && !chartPoints.isEmpty()) {
|
||||
int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2;
|
||||
double value = firstChartValueSeries.extractDataFromChartPoint(last);
|
||||
int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight();
|
||||
@@ -897,7 +894,7 @@ public class ChartView extends View {
|
||||
*/
|
||||
private void updateSeries() {
|
||||
synchronized (chartPoints) {
|
||||
seriesList.stream().forEach(this::updateSerie);
|
||||
seriesList.forEach(this::updateSerie);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.dennisguse.opentracks.data;
|
||||
|
||||
public record SelectionData(
|
||||
String selection,
|
||||
String[] selectionArgs
|
||||
String[] selectionArgs //TODO Refactor to List<String>
|
||||
) {
|
||||
|
||||
public SelectionData() {
|
||||
|
||||
@@ -386,7 +386,7 @@ public class TrackDataHub {
|
||||
lastSeenTrackPointId = localLastSeenTrackPointIdId;
|
||||
}
|
||||
|
||||
listeners.stream().forEach(Listener::onNewTrackPointsDone);
|
||||
listeners.forEach(Listener::onNewTrackPointsDone);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
@@ -67,10 +66,11 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
|
||||
}
|
||||
|
||||
// Builds selection arguments.
|
||||
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.id())).collect(Collectors.toCollection(ArrayList::new));
|
||||
ArrayList<String> args = new ArrayList<>();
|
||||
args.addAll(trackIds.stream().map(id -> Long.toString(id.id())).toList());
|
||||
args.addAll(categories);
|
||||
args.addAll(fromToArgs);
|
||||
selectionArgs = args.stream().toArray(String[]::new);
|
||||
selectionArgs = args.toArray(String[]::new);
|
||||
|
||||
return new SelectionData(selection, selectionArgs);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
@@ -118,7 +117,7 @@ public enum ActivityType {
|
||||
return Arrays.stream(values())
|
||||
.map(ActivityType::getLocalizedStringId)
|
||||
.map(context::getString)
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
||||
@@ -14,15 +14,15 @@ public class DistanceFormatter {
|
||||
|
||||
private final Resources resources;
|
||||
|
||||
private final int decimalCount;
|
||||
private final int decimalPlaces;
|
||||
|
||||
private final double threshold;
|
||||
|
||||
private final UnitSystem unitSystem;
|
||||
|
||||
private DistanceFormatter(Resources resources, int decimalCount, double threshold, UnitSystem unitSystem) {
|
||||
private DistanceFormatter(Resources resources, int decimalPlaces, double threshold, UnitSystem unitSystem) {
|
||||
this.resources = resources;
|
||||
this.decimalCount = decimalCount;
|
||||
this.decimalPlaces = decimalPlaces;
|
||||
this.threshold = threshold;
|
||||
this.unitSystem = unitSystem;
|
||||
assert unitSystem != null;
|
||||
@@ -57,30 +57,30 @@ public class DistanceFormatter {
|
||||
switch (unitSystem) {
|
||||
case METRIC -> {
|
||||
if (distance.greaterThan(Distance.ofKilometer(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalPlaces), resources.getString(R.string.unit_kilometer));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalPlaces), resources.getString(R.string.unit_meter));
|
||||
}
|
||||
}
|
||||
case IMPERIAL_FEET -> {
|
||||
if (distance.greaterThan(Distance.ofMile(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalPlaces), resources.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalPlaces), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
}
|
||||
case IMPERIAL_METER -> {
|
||||
if (distance.greaterThan(Distance.ofMile(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalPlaces), resources.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalPlaces), resources.getString(R.string.unit_meter));
|
||||
}
|
||||
}
|
||||
case NAUTICAL_IMPERIAL -> {
|
||||
if (distance.greaterThan(Distance.ofNauticalMile(threshold))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalCount), resources.getString(R.string.unit_nautical_mile));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toNauticalMiles(), decimalPlaces), resources.getString(R.string.unit_nautical_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalPlaces), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
}
|
||||
default -> throw new RuntimeException("Not implemented");
|
||||
@@ -93,19 +93,19 @@ public class DistanceFormatter {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private int decimalCount;
|
||||
private int decimalPlaces;
|
||||
|
||||
private UnitSystem unitSystem;
|
||||
|
||||
private double threshold;
|
||||
|
||||
public Builder() {
|
||||
decimalCount = 2;
|
||||
decimalPlaces = 2;
|
||||
threshold = 0.5;
|
||||
}
|
||||
|
||||
public Builder setDecimalCount(int decimalCount) {
|
||||
this.decimalCount = decimalCount;
|
||||
public Builder setDecimalPlaces(int decimalPlaces) {
|
||||
this.decimalPlaces = decimalPlaces;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class DistanceFormatter {
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Resources resource) {
|
||||
return new DistanceFormatter(resource, decimalCount, threshold, unitSystem);
|
||||
return new DistanceFormatter(resource, decimalPlaces, threshold, unitSystem);
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Context context) {
|
||||
|
||||
@@ -12,17 +12,16 @@ import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class SpeedFormatter {
|
||||
|
||||
private final Resources resources;
|
||||
private final int DECIMAL_PLACES = 1;
|
||||
|
||||
private final int decimalCount;
|
||||
private final Resources resources;
|
||||
|
||||
private final UnitSystem unitSystem;
|
||||
|
||||
private final boolean reportSpeedOrPace;
|
||||
|
||||
private SpeedFormatter(Resources resources, int decimalCount, UnitSystem unitSystem, boolean reportSpeedOrPace) {
|
||||
private SpeedFormatter(Resources resources, UnitSystem unitSystem, boolean reportSpeedOrPace) {
|
||||
this.resources = resources;
|
||||
this.decimalCount = decimalCount;
|
||||
this.unitSystem = unitSystem;
|
||||
this.reportSpeedOrPace = reportSpeedOrPace;
|
||||
}
|
||||
@@ -55,7 +54,7 @@ public class SpeedFormatter {
|
||||
}
|
||||
|
||||
if (reportSpeedOrPace) {
|
||||
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), 1), unitString);
|
||||
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), DECIMAL_PLACES), unitString);
|
||||
}
|
||||
|
||||
int pace = (int) speed.toPace(unitSystem).getSeconds();
|
||||
@@ -71,22 +70,14 @@ public class SpeedFormatter {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private int decimalCount;
|
||||
|
||||
private UnitSystem unitSystem;
|
||||
|
||||
private boolean reportSpeedOrPace;
|
||||
|
||||
public Builder() {
|
||||
decimalCount = 2;
|
||||
reportSpeedOrPace = true;
|
||||
}
|
||||
|
||||
public Builder setDecimalCount(int decimalCount) {
|
||||
this.decimalCount = decimalCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnit(@NonNull UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
return this;
|
||||
@@ -98,7 +89,7 @@ public class SpeedFormatter {
|
||||
}
|
||||
|
||||
public SpeedFormatter build(Resources resource) {
|
||||
return new SpeedFormatter(resource, decimalCount, unitSystem, reportSpeedOrPace);
|
||||
return new SpeedFormatter(resource, unitSystem, reportSpeedOrPace);
|
||||
}
|
||||
|
||||
public SpeedFormatter build(Context context) {
|
||||
|
||||
@@ -35,7 +35,7 @@ import java.util.Optional;
|
||||
public class TrackPoint {
|
||||
|
||||
@Nullable
|
||||
private TrackPoint.Id id;
|
||||
private final TrackPoint.Id id;
|
||||
|
||||
//Requires: position.time must be non-null
|
||||
@NonNull
|
||||
|
||||
@@ -23,7 +23,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
||||
|
||||
private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
|
||||
|
||||
@Deprecated
|
||||
public static void showDialog(FragmentManager fragmentManager, Context context, String activityTypeLocalized) {
|
||||
ActivityType activityType = ActivityType.findByLocalizedString(context, activityTypeLocalized);
|
||||
showDialog(fragmentManager, activityType);
|
||||
|
||||
@@ -62,7 +62,7 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
updateDataOnUI();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
|
||||
if (PreferencesUtils.isKey(List.of(R.string.stats_custom_layouts_key, R.string.stats_custom_layout_selected_layout_key), key)) {
|
||||
onLayoutChanged(PreferencesUtils.getCustomLayout());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
@@ -25,8 +23,7 @@ public class TrackFilenameGenerator {
|
||||
public static final String STARTTIME_DATE_KEY = "{date}";
|
||||
|
||||
public static String getAllOptions() {
|
||||
return Stream.of(UUID_KEY, TRACKNAME_KEY, ACTIVITY_TYPE_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY)
|
||||
.collect(Collectors.joining(", "));
|
||||
return String.join(", ", UUID_KEY, TRACKNAME_KEY, ACTIVITY_TYPE_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY);
|
||||
}
|
||||
|
||||
public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) {
|
||||
|
||||
@@ -149,12 +149,12 @@ public class CSVTrackExporter implements TrackExporter {
|
||||
printWriter = null;
|
||||
}
|
||||
|
||||
public void writeHeader(List<Column> columns) {
|
||||
private void writeHeader(List<Column> columns) {
|
||||
String columnNames = columns.stream().map(c -> c.columnName).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println("#" + columnNames);
|
||||
}
|
||||
|
||||
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
|
||||
private void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
|
||||
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint)).reduce((s, s2) -> s + "," + s2).orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println(columnNames);
|
||||
}
|
||||
|
||||
@@ -24,15 +24,19 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -41,14 +45,12 @@ import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
/**
|
||||
* An activity for saving tracks to the external storage.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
* TODO: This class needs some refactoring.
|
||||
* * It pushes each export job (usually one Track) for export; although export could be run in parallel.
|
||||
* Also this requires that the ExportActivity stays in foreground, so the user has to activily wait.
|
||||
* It would be better to let the ExportService handle this and let it report progress / conflicts to ExportActivity
|
||||
@@ -56,7 +58,9 @@ import de.dennisguse.opentracks.util.FileUtils;
|
||||
* So, for this check actually a different file name might be used than in the ExportService.
|
||||
* * Saved state as an object instead of individual values.
|
||||
*/
|
||||
public class ExportActivity extends AppCompatActivity implements ExportService.ExportServiceResultReceiver.Receiver {
|
||||
//TODO Show progress per exported track (currently per exported file)
|
||||
//TODO export tracks in parallel (for multiple files)
|
||||
public class ExportActivity extends AbstractActivity {
|
||||
|
||||
private static final String TAG = ExportActivity.class.getSimpleName();
|
||||
|
||||
@@ -84,8 +88,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
private TrackFileFormat trackFileFormat;
|
||||
private Uri directoryUri;
|
||||
|
||||
private ExportService.ExportServiceResultReceiver resultReceiver;
|
||||
|
||||
private List<String> directoryFiles;
|
||||
|
||||
private int trackExportSuccessCount;
|
||||
@@ -147,8 +149,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
|
||||
setContentView(viewBinding.getRoot());
|
||||
|
||||
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
|
||||
|
||||
@@ -161,8 +161,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
|
||||
String directoryDisplayName = FileUtils.getPath(documentFile);
|
||||
|
||||
resultReceiver = new ExportService.ExportServiceResultReceiver(new Handler(), this);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
autoConflict = ConflictResolutionStrategy.CONFLICT_NONE;
|
||||
setProgress();
|
||||
@@ -190,6 +188,13 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
viewBinding.exportActivityToolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected View createRootView() {
|
||||
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
@@ -207,6 +212,7 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
viewBinding = null;
|
||||
conflictsQueue.clear();
|
||||
exportTasks.clear();
|
||||
}
|
||||
@@ -216,9 +222,9 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
exportTasks = new ArrayList<>();
|
||||
if (allInOneFile) {
|
||||
String filename = "OpenTracks-Backup";
|
||||
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList())));
|
||||
exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).toList()));
|
||||
} else {
|
||||
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
|
||||
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).toList());
|
||||
}
|
||||
trackExportTotalCount = exportTasks.size();
|
||||
}
|
||||
@@ -231,19 +237,45 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
|
||||
if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) {
|
||||
conflict(exportTask);
|
||||
} else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
|
||||
return;
|
||||
}
|
||||
if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
|
||||
trackExportSkippedCount++;
|
||||
nextExport(exportTask);
|
||||
} else {
|
||||
ExportService.enqueue(this, resultReceiver, exportTask, directoryUri);
|
||||
return;
|
||||
}
|
||||
|
||||
WorkManager workManager = WorkManager.getInstance(this);
|
||||
WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class)
|
||||
.setInputData(new Data.Builder()
|
||||
.putLongArray(ExportWorker.TRACKIDS_KEY, exportTask.getTrackIds().stream().mapToLong(Track.Id::id).toArray())
|
||||
.putString(ExportWorker.DIRECTORY_URI_KEY, directoryUri.toString())
|
||||
.putString(ExportWorker.TRACKFILEFORMAT_KEY, exportTask.getTrackFileFormat().toString())
|
||||
.putString(ExportWorker.FILENAME_KEY, exportTask.getFilename())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
workManager
|
||||
.getWorkInfoByIdLiveData(exportRequest.getId())
|
||||
.observe(this, workInfo -> {
|
||||
if (workInfo != null) {
|
||||
WorkInfo.State state = workInfo.getState();
|
||||
switch (state) {
|
||||
case SUCCEEDED -> onExportSuccess(exportTask);
|
||||
case FAILED ->
|
||||
onExportError(exportTask, workInfo.getOutputData().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workManager.enqueue(exportRequest);
|
||||
}
|
||||
|
||||
private void export(ExportTask exportTask) {
|
||||
export(exportTask, autoConflict);
|
||||
}
|
||||
|
||||
@Deprecated //TODO Check should be done in ExportService
|
||||
@Deprecated //TODO Check should be done in ExportWorker
|
||||
private boolean exportFileExists(ExportTask exportTask) {
|
||||
String filename;
|
||||
if (exportTask.isMultiExport()) {
|
||||
@@ -314,7 +346,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExportSuccess(ExportTask exportTask) {
|
||||
if (exportFileExists(exportTask)) {
|
||||
trackExportOverwrittenCount++;
|
||||
@@ -325,7 +356,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
|
||||
nextExport(exportTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExportError(ExportTask exportTask, String errorMessage) {
|
||||
trackExportErrorCount++;
|
||||
String name;
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.app.job.JobService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
|
||||
public class ExportService extends JobIntentService {
|
||||
|
||||
private static final String TAG = ExportService.class.getSimpleName();
|
||||
|
||||
private static final int JOB_ID = 1;
|
||||
|
||||
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||
private static final String EXTRA_EXPORT_TASK = "export_task";
|
||||
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
|
||||
|
||||
public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) {
|
||||
Intent intent = new Intent(context, JobService.class);
|
||||
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||
intent.putExtra(EXTRA_EXPORT_TASK, exportTask);
|
||||
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
|
||||
enqueueWork(context, ExportService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
// Get all data.
|
||||
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||
ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK);
|
||||
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
|
||||
|
||||
// Prepare resultCode and bundle to send to the receiver.
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask);
|
||||
|
||||
// Build directory file.
|
||||
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
|
||||
if (directoryFile == null || !directoryFile.canWrite()) {
|
||||
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile);
|
||||
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Export and send result
|
||||
try {
|
||||
ExportUtils.exportTrack(this, directoryFile, exportTask);
|
||||
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_SUCCESS, bundle);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Export failed: " + e);
|
||||
e.printStackTrace(); //TODO Remove
|
||||
|
||||
bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, e.getMessage());
|
||||
resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExportServiceResultReceiver extends ResultReceiver {
|
||||
|
||||
public static final int RESULT_CODE_SUCCESS = 1;
|
||||
public static final int RESULT_CODE_ERROR = 0;
|
||||
|
||||
public static final String RESULT_EXTRA_EXPORT_TASK = "result_extra_export_task";
|
||||
|
||||
public static final String EXTRA_EXPORT_ERROR_MESSAGE = "extra_export_error_message";
|
||||
|
||||
private final Receiver receiver;
|
||||
|
||||
public ExportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
|
||||
super(handler);
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
ExportTask exportTask = resultData.getParcelable(RESULT_EXTRA_EXPORT_TASK);
|
||||
switch (resultCode) {
|
||||
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
|
||||
case RESULT_CODE_ERROR -> receiver.onExportError(exportTask, resultData.getString(EXTRA_EXPORT_ERROR_MESSAGE));
|
||||
default -> throw new RuntimeException("Unknown resultCode.");
|
||||
}
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
default void onExportSuccess(ExportTask exportTask) {
|
||||
}
|
||||
|
||||
default void onExportError(ExportTask exportTask, String errorMessage) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-19
@@ -1,60 +1,112 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||
import de.dennisguse.opentracks.io.file.exporter.ExportService;
|
||||
import de.dennisguse.opentracks.io.file.exporter.ExportService.ExportServiceResultReceiver;
|
||||
import de.dennisguse.opentracks.io.file.exporter.ExportTask;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
public class ExportUtils {
|
||||
|
||||
private static final String TAG = ExportUtils.class.getSimpleName();
|
||||
|
||||
public static void postWorkoutExport(Context context, Track.Id trackId) {
|
||||
public static void postWorkoutExport(AppCompatActivity context, Track.Id trackId) {
|
||||
if (PreferencesUtils.shouldInstantExportAfterWorkout()) {
|
||||
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
|
||||
DocumentFile directory = IntentUtils.toDocumentFile(context, PreferencesUtils.getDefaultExportDirectoryUri());
|
||||
|
||||
ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() {
|
||||
@Override
|
||||
public void onExportError(ExportTask unused, String errorMessage) {
|
||||
Intent intent = new Intent(context, SettingsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, errorMessage);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
});
|
||||
WorkManager workManager = WorkManager.getInstance(context);
|
||||
WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class)
|
||||
.setInputData(new Data.Builder()
|
||||
.putLongArray(ExportWorker.TRACKIDS_KEY, new long[]{trackId.id()})
|
||||
.putString(ExportWorker.DIRECTORY_URI_KEY, directory.toString())
|
||||
.putString(ExportWorker.TRACKFILEFORMAT_KEY, trackFileFormat.toString())
|
||||
.putString(ExportWorker.FILENAME_KEY, null)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
ExportService.enqueue(context, resultReceiver, new ExportTask(null, trackFileFormat, List.of(trackId)), directory.getUri());
|
||||
workManager
|
||||
.getWorkInfoByIdLiveData(exportRequest.getId())
|
||||
.observe(context, workInfo -> {
|
||||
if (workInfo != null) {
|
||||
if (workInfo.getState() == WorkInfo.State.FAILED) {
|
||||
Intent intent = new Intent(context, SettingsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, workInfo.getProgress().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workManager.enqueue(exportRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public static void exportTrack(Context context, DocumentFile directory, @Nullable String filenameForMultiple, TrackFileFormat trackFileFormat, List<Track.Id> trackIds) {
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
List<Track> tracks = trackIds.stream()
|
||||
.map(contentProviderUtils::getTrack)
|
||||
.toList();
|
||||
|
||||
Uri exportDocumentFileUri;
|
||||
if (tracks.size() == 1) {
|
||||
exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), trackFileFormat, directory);
|
||||
} else {
|
||||
String filename = TrackFilenameGenerator.format(filenameForMultiple, trackFileFormat);
|
||||
exportDocumentFileUri = getExportDocumentFileUri(context, filename, trackFileFormat, directory);
|
||||
}
|
||||
|
||||
if (exportDocumentFileUri == null) {
|
||||
throw new RuntimeException("Couldn't create document file for export");
|
||||
}
|
||||
|
||||
TrackExporter trackExporter = trackFileFormat.createTrackExporter(context, contentProviderUtils);
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri, "wt")) {
|
||||
if (!trackExporter.writeTrack(tracks, outputStream)) {
|
||||
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
|
||||
throw new RuntimeException("Unable to delete exportDocumentFile");
|
||||
}
|
||||
throw new RuntimeException("Unable to export track");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException("Unable to open exportDocumentFile " + exportDocumentFileUri, e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to close exportDocumentFile output stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static void exportTrack(Context context, DocumentFile directory, ExportTask exportTask) {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
List<Track> tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList());
|
||||
List<Track> tracks = exportTask.getTrackIds().stream()
|
||||
.map(contentProviderUtils::getTrack)
|
||||
.toList();
|
||||
Uri exportDocumentFileUri;
|
||||
if (tracks.size() == 1) {
|
||||
exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), exportTask.getTrackFileFormat(), directory);
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
public class ExportWorker extends Worker {
|
||||
|
||||
private static final String TAG = ExportWorker.class.getSimpleName();
|
||||
|
||||
static final String TRACKIDS_KEY = "TRACKIDS_KEY";
|
||||
static final String DIRECTORY_URI_KEY = "DIRECTORY_URI_KEY";
|
||||
static final String TRACKFILEFORMAT_KEY = "TRACKFILEFORMAT_KEY";
|
||||
static final String FILENAME_KEY = "FILENAME_KEY"; //optional; only used for multiple tracks into one file
|
||||
|
||||
static final String RESULT_EXPORT_ERROR_MESSAGE_KEY = "EXPORT_ERROR_MESSAGE";
|
||||
|
||||
private final List<Track.Id> trackIds;
|
||||
|
||||
private final DocumentFile directoryFile;
|
||||
|
||||
private final TrackFileFormat trackFileFormat;
|
||||
|
||||
private final String filename;
|
||||
|
||||
public ExportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
|
||||
trackIds = Arrays.stream(getInputData().getLongArray(TRACKIDS_KEY))
|
||||
.mapToObj(Track.Id::new)
|
||||
.toList();
|
||||
|
||||
directoryFile = DocumentFile.fromTreeUri(context, Uri.parse(getInputData().getString(DIRECTORY_URI_KEY)));
|
||||
|
||||
trackFileFormat = TrackFileFormat.valueOf(getInputData().getString(TRACKFILEFORMAT_KEY));
|
||||
|
||||
filename = getInputData().getString(FILENAME_KEY);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
if (directoryFile == null || !directoryFile.canWrite()) {
|
||||
return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, getApplicationContext().getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile).build());
|
||||
}
|
||||
|
||||
try {
|
||||
//TODO move method to ExportWorker?
|
||||
ExportUtils.exportTrack(getApplicationContext(), directoryFile, filename, trackFileFormat, trackIds);
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Export failed: " + e);
|
||||
|
||||
return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, e.getMessage()).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -4,7 +4,6 @@ import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.widget.Toast;
|
||||
@@ -149,9 +148,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
||||
super.configureDirectoryChooserIntent(intent);
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
if (PreferencesUtils.isDefaultExportDirectoryUri()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri());
|
||||
}
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,11 @@ import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackRecordedActivity;
|
||||
import de.dennisguse.opentracks.databinding.ImportActivityBinding;
|
||||
@@ -43,7 +41,7 @@ import de.dennisguse.opentracks.util.IntentUtils;
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ImportActivity extends FragmentActivity {
|
||||
public class ImportActivity extends AbstractActivity {
|
||||
|
||||
private static final String TAG = ImportActivity.class.getSimpleName();
|
||||
|
||||
@@ -57,14 +55,11 @@ public class ImportActivity extends FragmentActivity {
|
||||
private ArrayList<Uri> documentUris = new ArrayList<>();
|
||||
private boolean isDirectory;
|
||||
|
||||
private ImportViewModel viewModel;
|
||||
private ImportViewModel.Summary summary;
|
||||
private Importer importer;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
|
||||
setContentView(viewBinding.getRoot());
|
||||
|
||||
final Intent intent = getIntent();
|
||||
final ClipData intentClipData = intent.getClipData();
|
||||
@@ -96,21 +91,39 @@ public class ImportActivity extends FragmentActivity {
|
||||
documentFiles = new ArrayList<>();
|
||||
documentFiles.add(DocumentFile.fromTreeUri(this, documentUris.get(0)));
|
||||
} else {
|
||||
documentFiles = documentUris.stream().map(it -> DocumentFile.fromSingleUri(this, it)).collect(Collectors.toList());
|
||||
documentFiles = documentUris.stream()
|
||||
.map(it -> DocumentFile.fromSingleUri(this, it))
|
||||
.toList();
|
||||
}
|
||||
|
||||
initViews();
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
|
||||
viewModel.getImportData(documentFiles).observe(this, data -> {
|
||||
summary = data;
|
||||
setProgress();
|
||||
});
|
||||
importer = new Importer(this, documentFiles);
|
||||
importer.getLiveData()
|
||||
.observe(this, this::setProgress);
|
||||
importer.startImport();
|
||||
|
||||
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
|
||||
viewBinding.importActivityToolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected View createRootView() {
|
||||
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
viewBinding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
importer.cancel();
|
||||
return super.onSupportNavigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
@@ -118,21 +131,8 @@ public class ImportActivity extends FragmentActivity {
|
||||
outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory);
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
viewBinding.importProgressDone.setText("0");
|
||||
viewBinding.importProgressTotal.setText("0");
|
||||
viewBinding.importProgressSummaryOk.setText("0");
|
||||
viewBinding.importProgressSummaryExists.setText("0");
|
||||
viewBinding.importProgressSummaryErrors.setText("0");
|
||||
}
|
||||
|
||||
private int getTotalDone() {
|
||||
return summary != null ? summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount() : 0;
|
||||
}
|
||||
|
||||
private void setProgress() {
|
||||
int done = getTotalDone();
|
||||
|
||||
private void setProgress(Importer.Summary summary) {
|
||||
int done = summary.getCount();
|
||||
viewBinding.importProgressDone.setText("" + done);
|
||||
viewBinding.importProgressTotal.setText("" + summary.getTotalCount());
|
||||
|
||||
@@ -144,12 +144,12 @@ public class ImportActivity extends FragmentActivity {
|
||||
viewBinding.importProgressSummaryExistsGroup.setVisibility(summary.getExistsCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
viewBinding.importProgressSummaryErrorsGroup.setVisibility(summary.getErrorCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (done == summary.getTotalCount()) {
|
||||
onImportEnded();
|
||||
if (summary.isDone()) {
|
||||
onImportEnded(summary);
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportEnded() {
|
||||
private void onImportEnded(Importer.Summary summary) {
|
||||
viewBinding.importProgressAlertMsg.setVisibility(View.VISIBLE);
|
||||
viewBinding.importProgressAlertIcon.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -161,13 +161,13 @@ public class ImportActivity extends FragmentActivity {
|
||||
});
|
||||
|
||||
if (summary.getErrorCount() > 0) {
|
||||
toggleUIEndWithErrors();
|
||||
toggleUIEndWithErrors(summary);
|
||||
} else {
|
||||
toggleUIEndOk();
|
||||
toggleUIEndOk(summary);
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleUIEndWithErrors() {
|
||||
private void toggleUIEndWithErrors(Importer.Summary summary) {
|
||||
viewBinding.importProgressLeftButton.setVisibility(View.VISIBLE);
|
||||
viewBinding.importProgressLeftButton.setText(getString(R.string.generic_show_errors));
|
||||
viewBinding.importProgressLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.import_error_list_dialog_title), summary.getFileErrors()));
|
||||
@@ -175,7 +175,7 @@ public class ImportActivity extends FragmentActivity {
|
||||
viewBinding.importProgressAlertMsg.setText(getResources().getQuantityString(R.plurals.generic_completed_with_errors, summary.getErrorCount(), summary.getErrorCount()));
|
||||
}
|
||||
|
||||
private void toggleUIEndOk() {
|
||||
private void toggleUIEndOk(Importer.Summary summary) {
|
||||
viewBinding.importProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_dialog_success_24dp));
|
||||
viewBinding.importProgressAlertMsg.setText(getString(R.string.generic_completed));
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.app.job.JobService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportService extends JobIntentService {
|
||||
|
||||
private static final String TAG = ImportService.class.getSimpleName();
|
||||
|
||||
private static final int JOB_ID = 2;
|
||||
|
||||
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||
private static final String EXTRA_URI = "extra_uri";
|
||||
|
||||
private ResultReceiver resultReceiver;
|
||||
|
||||
public static void enqueue(Context context, ImportServiceResultReceiver receiver, Uri uri) {
|
||||
Intent intent = new Intent(context, JobService.class);
|
||||
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||
intent.putExtra(EXTRA_URI, uri);
|
||||
enqueueWork(context, ImportService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||
Uri uri = intent.getParcelableExtra(EXTRA_URI);
|
||||
importFile(DocumentFile.fromSingleUri(this, uri));
|
||||
}
|
||||
|
||||
private void importFile(DocumentFile file) {
|
||||
ArrayList<Track.Id> trackIds = new ArrayList<>();
|
||||
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
try {
|
||||
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
|
||||
boolean preventReimport = PreferencesUtils.getPreventReimportTracks();
|
||||
|
||||
TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), maxRecordingDistance, preventReimport);
|
||||
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new GPXTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new KMLTrackImporter(this, trackImporter)).importFile(this, file.getUri()));
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new KMZTrackImporter(this, trackImporter).importFile(file.getUri()));
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file format.");
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackIds.isEmpty()) {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName()));
|
||||
} else {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackIds, file, getString(R.string.import_unable_to_import_file, file.getName()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to import file", e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unable_to_import_file, e.getMessage()));
|
||||
} catch (ImportParserException e) {
|
||||
Log.d(TAG, "Parser error: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_parser_error, e.getMessage()));
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResult(int resultCode, ArrayList<Track.Id> trackId, DocumentFile file, String message) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID, trackId);
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName());
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
|
||||
resultReceiver.send(resultCode, bundle);
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.ResultReceiver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Create a new ResultReceive to receive results.
|
||||
* Your {@link #onReceiveResult} method will be called from the thread running <var>handler</var> if given, or from an arbitrary thread if null.
|
||||
*/
|
||||
public class ImportServiceResultReceiver extends ResultReceiver {
|
||||
|
||||
public static final int RESULT_CODE_ERROR = 0;
|
||||
public static final int RESULT_CODE_IMPORTED = 1;
|
||||
public static final int RESULT_CODE_ALREADY_EXISTS = 2;
|
||||
|
||||
public static final String RESULT_EXTRA_LIST_TRACK_ID = "result_track_id";
|
||||
public static final String RESULT_EXTRA_FILENAME = "result_extra_filename";
|
||||
public static final String RESULT_EXTRA_MESSAGE = "result_extra_message";
|
||||
|
||||
private final Receiver receiver;
|
||||
|
||||
public ImportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
|
||||
super(handler);
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
receiver.onReceiveResult(resultCode, resultData);
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
void onReceiveResult(int resultCode, Bundle resultData);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportViewModel extends AndroidViewModel implements ImportServiceResultReceiver.Receiver {
|
||||
|
||||
private static final String TAG = ImportViewModel.class.getSimpleName();
|
||||
|
||||
private MutableLiveData<Summary> importData;
|
||||
private final ImportServiceResultReceiver resultReceiver;
|
||||
private final Summary summary;
|
||||
private boolean cancel = false;
|
||||
private final List<DocumentFile> filesToImport = new ArrayList<>();
|
||||
|
||||
public ImportViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
resultReceiver = new ImportServiceResultReceiver(new Handler(), this);
|
||||
summary = new Summary();
|
||||
}
|
||||
|
||||
LiveData<Summary> getImportData(List<DocumentFile> documentFiles) {
|
||||
if (importData == null) {
|
||||
importData = new MutableLiveData<>();
|
||||
loadData(documentFiles);
|
||||
}
|
||||
return importData;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
private void loadData(List<DocumentFile> documentFiles) {
|
||||
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
|
||||
.map(FileUtils::getFiles)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<DocumentFile> fileList = new ArrayList<>();
|
||||
nestedFileList.forEach(fileList::addAll);
|
||||
|
||||
summary.totalCount = fileList.size();
|
||||
filesToImport.addAll(fileList);
|
||||
importNextFile();
|
||||
}
|
||||
|
||||
private void importNextFile() {
|
||||
if (cancel || filesToImport.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ImportService.enqueue(getApplication(), resultReceiver, filesToImport.get(0).getUri());
|
||||
filesToImport.remove(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (resultData == null) {
|
||||
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
|
||||
}
|
||||
|
||||
ArrayList<Track.Id> trackIds = resultData.getParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID);
|
||||
String fileName = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME);
|
||||
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
|
||||
|
||||
switch (resultCode) {
|
||||
case ImportServiceResultReceiver.RESULT_CODE_ERROR -> {
|
||||
summary.errorCount++;
|
||||
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
|
||||
}
|
||||
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED -> {
|
||||
summary.importedTrackIds.addAll(trackIds);
|
||||
summary.successCount++;
|
||||
}
|
||||
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS -> summary.existsCount++;
|
||||
default ->
|
||||
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
|
||||
}
|
||||
|
||||
importData.postValue(summary);
|
||||
importNextFile();
|
||||
}
|
||||
|
||||
static class Summary {
|
||||
private int totalCount;
|
||||
private int successCount;
|
||||
private int existsCount;
|
||||
private int errorCount;
|
||||
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
|
||||
private final ArrayList<String> fileErrors = new ArrayList<>();
|
||||
|
||||
public int getTotalCount() {
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public int getExistsCount() {
|
||||
return existsCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public ArrayList<Track.Id> getImportedTrackIds() {
|
||||
return importedTrackIds;
|
||||
}
|
||||
|
||||
public ArrayList<String> getFileErrors() {
|
||||
return fileErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportWorker extends Worker {
|
||||
|
||||
private static final String TAG = ImportWorker.class.getSimpleName();
|
||||
|
||||
static final String URI_KEY = "DIRECTORY_URI_KEY";
|
||||
|
||||
static final String RESULT_SUCCESS_LIST_TRACKIDS_KEY = "RESULT_TRACK_IDS";
|
||||
static final String RESULT_URI_KEY = "RESULT_URI";
|
||||
static final String RESULT_MESSAGE_KEY = "RESULT_MESSAGE";
|
||||
static final String RESULT_FAILURE_IS_DUPLICATE = "RESULT_FAILURE_IS_DUPLICATE";
|
||||
|
||||
private final Uri uri;
|
||||
|
||||
public ImportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
|
||||
uri = Uri.parse(getInputData().getString(URI_KEY));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
ArrayList<Track.Id> trackIds = new ArrayList<>();
|
||||
Context context = getApplicationContext();
|
||||
|
||||
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance();
|
||||
boolean preventReimport = PreferencesUtils.getPreventReimportTracks();
|
||||
TrackImporter trackImporter = new TrackImporter(context, new ContentProviderUtils(context), maxRecordingDistance, preventReimport);
|
||||
|
||||
Data.Builder data = new Data.Builder()
|
||||
.putString(RESULT_URI_KEY, uri.toString());
|
||||
|
||||
|
||||
String fileExtension = FileUtils.getExtension(uri);
|
||||
try {
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new GPXTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new XMLImporter(new KMLTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri));
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackIds.addAll(new KMZTrackImporter(context, trackImporter).importFile(uri));
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file format.");
|
||||
return Result.failure(data
|
||||
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unsupported_format))
|
||||
.build());
|
||||
}
|
||||
|
||||
if (!trackIds.isEmpty()) {
|
||||
return Result.success(data
|
||||
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_file_imported, uri.toString())) //TODO unused?
|
||||
.putLongArray(RESULT_SUCCESS_LIST_TRACKIDS_KEY, trackIds.stream().mapToLong(Track.Id::id).toArray())
|
||||
.build());
|
||||
}
|
||||
|
||||
return Result.failure(data
|
||||
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, uri.toString()))
|
||||
.build());
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to import file", e);
|
||||
return Result.failure(data
|
||||
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, e.getMessage()))
|
||||
.build());
|
||||
} catch (ImportParserException e) {
|
||||
Log.d(TAG, "Parser error: " + e.getMessage(), e);
|
||||
return Result.failure(data
|
||||
.putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_parser_error, e.getMessage()))
|
||||
.build());
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
|
||||
return Result.failure(data
|
||||
.putString(RESULT_MESSAGE_KEY, e.getMessage()) //TODO This is not used
|
||||
.putBoolean(RESULT_FAILURE_IS_DUPLICATE, true)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.WorkRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
//TODO Cancel does not abort current import
|
||||
//TODO Provide some intermediate progress (not only when a file got imported)
|
||||
public class Importer {
|
||||
|
||||
private final MutableLiveData<Summary> liveData = new MutableLiveData<>();
|
||||
private final Summary summary = new Summary();
|
||||
private final List<DocumentFile> filesToImport = new ArrayList<>();
|
||||
|
||||
private boolean cancel = false;
|
||||
|
||||
private final ImportActivity context;
|
||||
private final WorkManager workManager;
|
||||
|
||||
//TODO Add summary callback
|
||||
Importer(ImportActivity context, List<DocumentFile> documentFiles) {
|
||||
this.context = context;
|
||||
this.workManager = WorkManager.getInstance(context);
|
||||
|
||||
List<ArrayList<DocumentFile>> nestedFileList = documentFiles.stream()
|
||||
.map(FileUtils::getFiles)
|
||||
.toList();
|
||||
|
||||
List<DocumentFile> fileList = new ArrayList<>();
|
||||
nestedFileList.forEach(fileList::addAll);
|
||||
|
||||
summary.totalCount = fileList.size();
|
||||
filesToImport.addAll(fileList);
|
||||
}
|
||||
|
||||
LiveData<Summary> getLiveData() {
|
||||
return liveData;
|
||||
}
|
||||
|
||||
void startImport() {
|
||||
liveData.postValue(summary);
|
||||
importNextFile();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
private void importNextFile() {
|
||||
if (cancel || filesToImport.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final DocumentFile documentFile = filesToImport.get(0);
|
||||
|
||||
WorkRequest importRequest = new OneTimeWorkRequest.Builder(ImportWorker.class)
|
||||
.setInputData(new Data.Builder()
|
||||
.putString(ImportWorker.URI_KEY, documentFile.getUri().toString())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
workManager
|
||||
.getWorkInfoByIdLiveData(importRequest.getId())
|
||||
.observe(context, workInfo -> {
|
||||
if (workInfo != null) {
|
||||
WorkInfo.State state = workInfo.getState();
|
||||
if (state.isFinished()) {
|
||||
switch (state) {
|
||||
case SUCCEEDED -> {
|
||||
summary.importedTrackIds.addAll(
|
||||
Arrays.stream(workInfo.getOutputData().getLongArray(ImportWorker.RESULT_SUCCESS_LIST_TRACKIDS_KEY))
|
||||
.mapToObj(Track.Id::new)
|
||||
.toList());
|
||||
|
||||
summary.successCount++;
|
||||
}
|
||||
case FAILED -> {
|
||||
if (workInfo.getOutputData().getBoolean(ImportWorker.RESULT_FAILURE_IS_DUPLICATE, false)) {
|
||||
summary.existsCount++;
|
||||
} else {
|
||||
// Some error happened
|
||||
String errorMessage = workInfo.getOutputData().getString(ImportWorker.RESULT_MESSAGE_KEY);
|
||||
summary.fileErrors.add(context.getString(R.string.import_error_info, documentFile.getName(), errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
liveData.postValue(summary);
|
||||
importNextFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
workManager.enqueue(importRequest);
|
||||
filesToImport.remove(0);
|
||||
}
|
||||
|
||||
static class Summary {
|
||||
private int totalCount;
|
||||
private int successCount;
|
||||
private int existsCount;
|
||||
private final ArrayList<Track.Id> importedTrackIds = new ArrayList<>();
|
||||
private final ArrayList<String> fileErrors = new ArrayList<>();
|
||||
|
||||
public int getTotalCount() {
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public int getExistsCount() {
|
||||
return existsCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return fileErrors.size();
|
||||
}
|
||||
|
||||
public ArrayList<Track.Id> getImportedTrackIds() {
|
||||
return importedTrackIds;
|
||||
}
|
||||
|
||||
public ArrayList<String> getFileErrors() {
|
||||
return fileErrors;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return getSuccessCount() + getExistsCount() + getErrorCount();
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return getTotalCount() == getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
|
||||
public class StopRecording extends AbstractAPIActivity {
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
/**
|
||||
* {@link Track} and {@link TrackPoint} must be immutable (i.e., their content does not change).
|
||||
*/
|
||||
public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet) {
|
||||
|
||||
public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet, TrackStatistics currentSegment) {
|
||||
public String getTrackCategory() {
|
||||
if (track == null) {
|
||||
return "";
|
||||
@@ -20,11 +19,21 @@ public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorData
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TrackStatistics getTrackStatistics() {
|
||||
public TrackStatistics getStatisticsTrack() {
|
||||
if (track == null) {
|
||||
return new TrackStatistics();
|
||||
}
|
||||
|
||||
return track.getTrackStatistics();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TrackStatistics getStatisticsCurrentSegment() {
|
||||
if (currentSegment == null) {
|
||||
return new TrackStatistics();
|
||||
}
|
||||
|
||||
return currentSegment;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.app.job.JobService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.ResultReceiver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
|
||||
public class TrackDeleteService extends JobIntentService {
|
||||
|
||||
private static final int JOB_ID = 3;
|
||||
|
||||
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||
|
||||
private static final String EXTRA_TRACK_IDS = "extra_track_ids";
|
||||
|
||||
public static void enqueue(Context context, TrackDeleteResultReceiver receiver, ArrayList<Track.Id> toBeDeleted) {
|
||||
Intent intent = new Intent(context, JobService.class);
|
||||
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||
intent.putParcelableArrayListExtra(EXTRA_TRACK_IDS, toBeDeleted);
|
||||
enqueueWork(context, TrackDeleteService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
contentProviderUtils.deleteTracks(this, trackIds);
|
||||
|
||||
resultReceiver.send(TrackDeleteResultReceiver.RESULT_CODE_SUCCESS, new Bundle());
|
||||
}
|
||||
|
||||
public static class TrackDeleteResultReceiver extends ResultReceiver {
|
||||
|
||||
public static final int RESULT_CODE_SUCCESS = 1;
|
||||
|
||||
private final Receiver receiver;
|
||||
|
||||
public TrackDeleteResultReceiver(Handler handler, @NonNull Receiver receiver) {
|
||||
super(handler);
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
switch (resultCode) {
|
||||
case RESULT_CODE_SUCCESS -> receiver.onDeleteFinished();
|
||||
default -> throw new RuntimeException("Unknown resultCode.");
|
||||
}
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
void onDeleteFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
|
||||
public class TrackDeletionWorker extends Worker {
|
||||
|
||||
public static final String TRACKIDS_KEY = "TRACKIDS_KEY";
|
||||
|
||||
private final List<Track.Id> trackIds;
|
||||
|
||||
public TrackDeletionWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||
super(context, params);
|
||||
|
||||
trackIds = Arrays.stream(getInputData().getLongArray(TRACKIDS_KEY))
|
||||
.mapToObj(Track.Id::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
new ContentProviderUtils(getApplicationContext()).deleteTracks(getApplicationContext(), trackIds);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
reset();
|
||||
}
|
||||
|
||||
Pair<Track, Pair<TrackPoint, SensorDataSet>> getDataForUI() {
|
||||
RecordingData getDataForUI() {
|
||||
TrackStatisticsUpdater tmpTrackStatisticsUpdater = new TrackStatisticsUpdater(trackStatisticsUpdater);
|
||||
Pair<TrackPoint, SensorDataSet> current = trackPointCreator.createCurrentTrackPoint(lastTrackPointUIWithSpeed, lastTrackPointUIWithAltitude, lastStoredTrackPointWithLocation);
|
||||
|
||||
@@ -131,8 +131,9 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
|
||||
track.setTrackStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
||||
TrackStatistics currentSegment = tmpTrackStatisticsUpdater.getCurrentSegment();
|
||||
|
||||
return new Pair<>(track, current);
|
||||
return new RecordingData(track, current.first, current.second, currentSegment);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
@@ -42,7 +41,6 @@ import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
@@ -55,7 +53,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
||||
|
||||
public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null);
|
||||
public static final GpsStatusValue STATUS_GPS_DEFAULT = GpsStatusValue.GPS_NONE;
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
@@ -197,6 +195,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
startSensors();
|
||||
|
||||
voiceAnnouncementManager.start(trackRecordingManager.getTrackStatistics());
|
||||
|
||||
notificationManager.onRecordingStarted(this, recordingStatus.trackId());
|
||||
}
|
||||
|
||||
public void tryStartSensors() {
|
||||
@@ -216,7 +216,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
|
||||
trackPointCreator.start(this, handler);
|
||||
|
||||
ServiceCompat.startForeground(this, TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
ServiceCompat.startForeground(this, TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.onSensorsStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
|
||||
}
|
||||
|
||||
public void endCurrentTrack() {
|
||||
@@ -240,7 +240,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
void stopSensors() {
|
||||
trackPointCreator.stop();
|
||||
stopForeground(true);
|
||||
notificationManager.cancelNotification();
|
||||
notificationManager.stop();
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
gpsStatusObservable.postValue(STATUS_GPS_DEFAULT);
|
||||
}
|
||||
@@ -267,7 +267,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
}
|
||||
|
||||
boolean stored = trackRecordingManager.onNewTrackPoint(trackPoint);
|
||||
notificationManager.updateTrackPoint(this, trackRecordingManager.getTrackStatistics(), trackPoint, thresholdHorizontalAccuracy);
|
||||
//TODO Should be done during updateRecordingDataWhileRecording()
|
||||
notificationManager.updateTrackPoint(this, trackRecordingManager.getDataForUI(), thresholdHorizontalAccuracy);
|
||||
return stored;
|
||||
}
|
||||
|
||||
@@ -284,7 +285,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
Log.e(TAG, e.getMessage() + " " + writer);
|
||||
return;
|
||||
}
|
||||
notificationManager.updateContent(getString(gpsStatusValue.message));
|
||||
notificationManager.updateGPSStatus(getString(gpsStatusValue.message));
|
||||
gpsStatusObservable.postValue(gpsStatusValue);
|
||||
}
|
||||
|
||||
@@ -315,11 +316,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
}
|
||||
|
||||
// Compute temporary track statistics using sensorData and update time.
|
||||
Pair<Track, Pair<TrackPoint, SensorDataSet>> data = trackRecordingManager.getDataForUI();
|
||||
RecordingData data = trackRecordingManager.getDataForUI();
|
||||
|
||||
voiceAnnouncementManager.announceStatisticsIfNeeded(data.first, data.second.second);
|
||||
voiceAnnouncementManager.announceStatisticsIfNeeded(data.track(), data.sensorDataSet());
|
||||
|
||||
recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second));
|
||||
recordingDataObservable.postValue(data);
|
||||
}
|
||||
|
||||
public void onIdle() {
|
||||
|
||||
+60
-52
@@ -9,19 +9,20 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
@@ -43,15 +44,13 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
|
||||
TrackRecordingServiceNotificationManager(Context context) {
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
notificationChannel.setAllowBubbles(true);
|
||||
}
|
||||
|
||||
notificationManager.createNotificationChannel(notificationChannel);
|
||||
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
notificationChannel.setAllowBubbles(true);
|
||||
}
|
||||
|
||||
notificationManager.createNotificationChannel(notificationChannel);
|
||||
|
||||
notificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID);
|
||||
notificationBuilder
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
@@ -62,57 +61,21 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
.setSmallIcon(R.drawable.ic_logo_color_24dp);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
cancelNotification();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
TrackRecordingServiceNotificationManager(NotificationManager notificationManager, NotificationCompat.Builder notificationBuilder) {
|
||||
this.notificationManager = notificationManager;
|
||||
this.notificationBuilder = notificationBuilder;
|
||||
}
|
||||
|
||||
void updateContent(String content) {
|
||||
notificationBuilder.setSubText(content);
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) {
|
||||
String formattedAccuracy = context.getString(R.string.value_none);
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
// The decision if a trackpoint is not accurate enough should happen in the sensor subsystem.
|
||||
if (trackPoint.hasHorizontalAccuracy()) {
|
||||
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
|
||||
|
||||
boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy);
|
||||
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
|
||||
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
|
||||
previousLocationWasAccurate = currentLocationWasAccurate;
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
|
||||
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
|
||||
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
|
||||
updateNotification();
|
||||
|
||||
notificationBuilder.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
Notification setGPSonlyStarted(Context context) {
|
||||
Notification onSensorsStarted(Context context) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackListActivity.class);
|
||||
|
||||
int pendingIntentFlags = 0;
|
||||
pendingIntentFlags = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
PendingIntent pendingIntent = TaskStackBuilder.create(context)
|
||||
.addParentStack(TrackListActivity.class)
|
||||
.addNextIntent(intent)
|
||||
.getPendingIntent(0, pendingIntentFlags);
|
||||
.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
updateContent(context.getString(R.string.gps_starting));
|
||||
updateGPSStatus(context.getString(R.string.gps_starting));
|
||||
|
||||
notificationBuilder.setContentIntent(pendingIntent);
|
||||
updateNotification();
|
||||
@@ -120,12 +83,52 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
return getNotification();
|
||||
}
|
||||
|
||||
void cancelNotification() {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
void updateGPSStatus(String content) {
|
||||
notificationBuilder.setSubText(content);
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
void setUnitSystem(UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
public void onRecordingStarted(Context context, @NonNull Track.Id trackId) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
|
||||
PendingIntent pendingIntent = TaskStackBuilder.create(context)
|
||||
.addParentStack(TrackListActivity.class)
|
||||
.addNextIntent(intent)
|
||||
.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
notificationBuilder.setContentIntent(pendingIntent);
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
void updateTrackPoint(Context context, RecordingData recordingData, Distance thresholdHorizontalAccuracy) {
|
||||
String formattedAccuracy = context.getString(R.string.value_none);
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
|
||||
//TODO The decision if a trackpoint is not accurate enough should happen in the TrackRecordingManager (via RecordingData).
|
||||
if (recordingData.latestTrackPoint().hasHorizontalAccuracy()) {
|
||||
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().getHorizontalAccuracy());
|
||||
|
||||
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().getHorizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
|
||||
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
|
||||
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
|
||||
previousLocationWasAccurate = currentLocationWasAccurate;
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(recordingData.getStatisticsTrack().getTotalDistance())));
|
||||
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().getSpeed());
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
|
||||
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
|
||||
updateNotification();
|
||||
|
||||
notificationBuilder.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
private void updateNotification() {
|
||||
@@ -136,6 +139,11 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
return notificationBuilder.build();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setUnitSystem(UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import androidx.mediarouter.media.MediaRouter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
@@ -219,7 +220,7 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
|
||||
setFrequency(PreferencesUtils.getVoiceAnnouncementFrequency());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(new int[]{R.string.voice_announcement_distance_key, R.string.stats_units_key}, key)) {
|
||||
if (PreferencesUtils.isKey(List.of(R.string.voice_announcement_distance_key, R.string.stats_units_key), key)) {
|
||||
setFrequency(PreferencesUtils.getVoiceAnnouncementDistance());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import de.dennisguse.opentracks.R;
|
||||
|
||||
public class LocalePreference extends ListPreference {
|
||||
|
||||
private String TAG = LocalePreference.class.getSimpleName();
|
||||
private final String TAG = LocalePreference.class.getSimpleName();
|
||||
|
||||
public LocalePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.settings;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
|
||||
@@ -59,7 +58,7 @@ class PreferencesOpenHelper {
|
||||
if (!parts.get(1).matches("\\d+")) {
|
||||
parts.add(1, String.valueOf(PreferencesUtils.getLayoutColumnsByDefault()));
|
||||
}
|
||||
PreferencesUtils.setString(R.string.stats_custom_layouts_key, parts.stream().collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR)));
|
||||
PreferencesUtils.setString(R.string.stats_custom_layouts_key, String.join(CsvLayoutUtils.ITEM_SEPARATOR, parts));
|
||||
}
|
||||
|
||||
private void onDowngrade() {
|
||||
|
||||
@@ -34,7 +34,6 @@ import androidx.preference.PreferenceManager;
|
||||
import com.google.android.material.color.DynamicColors;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -61,6 +60,7 @@ import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
//TODO Separate layout-related preferences into a class
|
||||
public class PreferencesUtils {
|
||||
|
||||
private final static String TAG = PreferencesUtils.class.getSimpleName();
|
||||
@@ -125,7 +125,7 @@ public class PreferencesUtils {
|
||||
return key == null || key.equals(getKey(keyId));
|
||||
}
|
||||
|
||||
public static boolean isKey(int[] keyIds, String key) {
|
||||
public static boolean isKey(List<Integer> keyIds, String key) {
|
||||
for (int keyId : keyIds) {
|
||||
if (isKey(keyId, key)) {
|
||||
return true;
|
||||
@@ -396,7 +396,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
@@ -503,7 +503,7 @@ public class PreferencesUtils {
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
@@ -546,7 +546,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
@@ -619,7 +619,7 @@ public class PreferencesUtils {
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
@@ -808,7 +808,7 @@ public class PreferencesUtils {
|
||||
return resources.getInteger(R.integer.stats_custom_layout_columns_default);
|
||||
}
|
||||
|
||||
private static List<TypedArray> getMultiTypedArray() {
|
||||
private static List<TypedArray> getLayoutDataFields() {
|
||||
return Stream.of(
|
||||
R.array.stats_custom_layout_fields_default_value_0,
|
||||
R.array.stats_custom_layout_fields_default_value_1,
|
||||
@@ -828,13 +828,15 @@ public class PreferencesUtils {
|
||||
R.array.stats_custom_layout_fields_default_value_15,
|
||||
R.array.stats_custom_layout_fields_default_value_16,
|
||||
R.array.stats_custom_layout_fields_default_value_17,
|
||||
R.array.stats_custom_layout_fields_default_value_18
|
||||
).map(id -> resources.obtainTypedArray(id)).collect(Collectors.toList());
|
||||
R.array.stats_custom_layout_fields_default_value_18,
|
||||
R.array.stats_custom_layout_fields_default_value_19
|
||||
).map(id -> resources.obtainTypedArray(id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
private static String buildDefaultFields() {
|
||||
List<TypedArray> fieldsArrays = getMultiTypedArray();
|
||||
List<TypedArray> fieldsArrays = getLayoutDataFields();
|
||||
return fieldsArrays.stream().map(i -> i.getString(0) + CsvLayoutUtils.PROPERTY_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR))
|
||||
+ CsvLayoutUtils.ITEM_SEPARATOR;
|
||||
}
|
||||
@@ -893,18 +895,16 @@ public class PreferencesUtils {
|
||||
}
|
||||
|
||||
public static List<RecordingLayout> getAllCustomLayouts() {
|
||||
List<RecordingLayout> recordingLayouts = new ArrayList<>();
|
||||
String csvCustomLayout = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||
String[] csvLines = csvCustomLayout.split(CsvLayoutUtils.LINE_SEPARATOR);
|
||||
for (String line : csvLines) {
|
||||
recordingLayouts.add(RecordingLayoutIO.fromCsv(line, resources));
|
||||
}
|
||||
|
||||
return recordingLayouts;
|
||||
return Arrays.stream(csvLines).map(line -> RecordingLayoutIO.fromCsv(line, resources)).toList();
|
||||
}
|
||||
|
||||
public static List<String> getAllCustomLayoutNames() {
|
||||
return getAllCustomLayouts().stream().map(RecordingLayout::getName).collect(Collectors.toList());
|
||||
return getAllCustomLayouts().stream()
|
||||
.map(RecordingLayout::getName)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static void resetCustomLayoutPreferences() {
|
||||
@@ -915,13 +915,6 @@ public class PreferencesUtils {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Check if resetPreferences can be used instead.
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public static void clear() {
|
||||
sharedPreferences.edit().clear().commit();
|
||||
}
|
||||
|
||||
public static int getTotalRowsDeleted() {
|
||||
return getInt(R.string.total_rows_deleted_key, 0);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat {
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public class SensorsSettingsFragment extends PreferenceFragmentCompat {
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
|
||||
+2
-2
@@ -62,8 +62,8 @@ public class SettingsCustomLayoutEditActivity extends AbstractActivity implement
|
||||
ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END, 0) {
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition = target.getAbsoluteAdapterPosition();
|
||||
recordingLayoutFieldsVisible = adapterFieldsVisible.move(fromPosition, toPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ public class SettingsCustomLayoutListActivity extends AbstractActivity implement
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final int position = viewHolder.getAbsoluteAdapterPosition();
|
||||
final RecordingLayout item = adapter.getLayouts().get(position);
|
||||
|
||||
adapter.removeLayout(position);
|
||||
|
||||
+3
-4
@@ -24,7 +24,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
@@ -126,7 +125,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results) {
|
||||
listAdapter.addAll(results.stream().map(ScanResult::getDevice).collect(Collectors.toList()));
|
||||
listAdapter.addAll(results.stream().map(ScanResult::getDevice).toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,7 +148,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream()
|
||||
.map(ServiceMeasurementUUID::serviceUUID)
|
||||
.map(ParcelUuid::new)
|
||||
.collect(Collectors.toList())));
|
||||
.toList()));
|
||||
b.putBoolean(ARG_INCLUDE_INTERNAL, includeInternalSensor);
|
||||
|
||||
fragment.setArguments(b);
|
||||
@@ -224,7 +223,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
if (PreferencesUtils.getBluetoothFilterEnabled()) {
|
||||
scanFilter = serviceUUIDs.stream()
|
||||
.map(it -> new ScanFilter.Builder().setServiceUuid(it).build())
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
}
|
||||
|
||||
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ShareUtils {
|
||||
String trackDescription = "";
|
||||
if (trackIds.length == 1) {
|
||||
Track track = contentProviderUtils.getTrack(trackIds[0]);
|
||||
trackDescription = track == null ? "" : new DescriptionGenerator(context).generateTrackDescription(track, false);
|
||||
trackDescription = track == null ? "" : new TrackShareSummaryGenerator(context).generateTrackDescription(track);
|
||||
}
|
||||
|
||||
String mime = "";
|
||||
|
||||
+41
-104
@@ -35,20 +35,17 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Generates descriptions for tracks and markers.
|
||||
* Generates descriptions for tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DescriptionGenerator {
|
||||
public class TrackShareSummaryGenerator {
|
||||
|
||||
private static final String HTML_LINE_BREAK = "<br>";
|
||||
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
|
||||
private static final String TEXT_LINE_BREAK = "\n";
|
||||
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DescriptionGenerator(Context context) {
|
||||
public TrackShareSummaryGenerator(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@@ -56,160 +53,106 @@ public class DescriptionGenerator {
|
||||
* Generates a track description.
|
||||
*
|
||||
* @param track the track
|
||||
* @param html true to output html, false to output plain text
|
||||
*/
|
||||
public String generateTrackDescription(Track track, boolean html) {
|
||||
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
|
||||
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
|
||||
public String generateTrackDescription(Track track) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Created by
|
||||
String creator = html
|
||||
? "<a href='" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
|
||||
: context.getString(R.string.app_name);
|
||||
builder.append(creator);
|
||||
builder.append(context.getString(R.string.app_name));
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
|
||||
builder.append(paragraphSeparator);
|
||||
|
||||
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
|
||||
writeString(track.getActivityTypeLocalized(), builder, R.string.description_activity_type, lineBreak);
|
||||
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
|
||||
builder.append(generateTrackStatisticsDescription(track.getTrackStatistics(), html));
|
||||
writeString(track.getName(), builder, R.string.generic_name_line);
|
||||
writeString(track.getActivityTypeLocalized(), builder, R.string.description_activity_type);
|
||||
writeString(track.getDescription(), builder, R.string.generic_description_line);
|
||||
builder.append(generateTrackStatisticsDescription(track.getTrackStatistics()));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to a string builder.
|
||||
*
|
||||
* @param text the string
|
||||
* @param builder the string builder
|
||||
* @param resId the resource id containing one string placeholder
|
||||
* @param lineBreak the line break
|
||||
*/
|
||||
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
|
||||
if (text == null || text.length() == 0) {
|
||||
private void writeString(String text, StringBuilder builder, int resId) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
text = context.getString(R.string.value_unknown);
|
||||
}
|
||||
builder.append(context.getString(resId, text));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TrackShareSummaryGenerator.TEXT_LINE_BREAK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a {@link TrackStatistics}.
|
||||
*
|
||||
* @param stats the track statistics
|
||||
* @param html true to use "<br>" for line break instead of "\n"
|
||||
*/
|
||||
private String generateTrackStatisticsDescription(TrackStatistics stats, boolean html) {
|
||||
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
|
||||
private String generateTrackStatisticsDescription(TrackStatistics stats) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Total distance
|
||||
writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
|
||||
writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance);
|
||||
|
||||
// Total time
|
||||
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
|
||||
writeTime(stats.getTotalTime(), builder, R.string.description_total_time);
|
||||
|
||||
// Moving time
|
||||
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
|
||||
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time);
|
||||
|
||||
// Average speed
|
||||
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
|
||||
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed);
|
||||
|
||||
// Average moving speed
|
||||
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, lineBreak);
|
||||
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed);
|
||||
|
||||
// Max speed
|
||||
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
|
||||
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed);
|
||||
|
||||
// Average pace
|
||||
writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
|
||||
writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute);
|
||||
|
||||
// Average moving pace
|
||||
writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute, lineBreak);
|
||||
writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute);
|
||||
|
||||
// Fastest pace
|
||||
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
|
||||
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute);
|
||||
|
||||
// Max altitude
|
||||
if (stats.hasAltitudeMax()) {
|
||||
writeAltitude(stats.getMaxAltitude(), builder, R.string.description_max_altitude, lineBreak);
|
||||
}
|
||||
|
||||
// Min altitude
|
||||
if (stats.hasAltitudeMin()) {
|
||||
writeAltitude(stats.getMinAltitude(), builder, R.string.description_min_altitude, lineBreak);
|
||||
// Min and Max altitude
|
||||
if (stats.hasAltitudeExtremities()) {
|
||||
writeAltitude(stats.getMaxAltitude(), builder, R.string.description_max_altitude);
|
||||
writeAltitude(stats.getMinAltitude(), builder, R.string.description_min_altitude);
|
||||
}
|
||||
|
||||
// Altitude gain
|
||||
if (stats.hasTotalAltitudeGain()) {
|
||||
writeAltitude(stats.getTotalAltitudeGain(), builder, R.string.description_altitude_gain, lineBreak);
|
||||
writeAltitude(stats.getTotalAltitudeGain(), builder, R.string.description_altitude_gain);
|
||||
}
|
||||
|
||||
// Altitude loss
|
||||
if (stats.hasTotalAltitudeLoss()) {
|
||||
writeAltitude(stats.getTotalAltitudeLoss(), builder, R.string.description_altitude_loss, lineBreak);
|
||||
writeAltitude(stats.getTotalAltitudeLoss(), builder, R.string.description_altitude_loss);
|
||||
}
|
||||
|
||||
// Recorded time
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTimeWithOffset(OffsetDateTime.ofInstant(stats.getStartTime(), ZoneId.systemDefault()))));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes distance.
|
||||
*
|
||||
* @param distance distance in meters
|
||||
* @param builder StringBuilder to append distance
|
||||
* @param resId resource id of distance string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeDistance(Distance distance, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writeDistance(Distance distance, StringBuilder builder, int resId) {
|
||||
double distanceInKm = distance.toKM();
|
||||
double distanceInMi = distance.toMI();
|
||||
builder.append(context.getString(resId, distanceInKm, distanceInMi));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes time.
|
||||
*
|
||||
* @param time time in milliseconds.
|
||||
* @param builder StringBuilder to append time
|
||||
* @param resId resource id of time string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeTime(Duration time, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writeTime(Duration time, StringBuilder builder, int resId) {
|
||||
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes speed.
|
||||
*
|
||||
* @param speed speed in meters per second
|
||||
* @param builder StringBuilder to append speed
|
||||
* @param resId resource id of speed string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeSpeed(Speed speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writeSpeed(Speed speed, StringBuilder builder, int resId) {
|
||||
builder.append(context.getString(resId, speed.toKMH(), speed.toMPH()));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param builder StringBuilder to append pace
|
||||
* @param resId resource id of pace string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writePace(Speed speed, StringBuilder builder, int resId) {
|
||||
Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setUnit(UnitSystem.METRIC).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
|
||||
Pair<String, String> paceInImperial = SpeedFormatter.Builder().setUnit(UnitSystem.IMPERIAL_FEET).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
|
||||
|
||||
@@ -217,20 +160,14 @@ public class DescriptionGenerator {
|
||||
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
|
||||
|
||||
builder.append(context.getString(resId, formattedPaceMetrics, formattedPaceImperial));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param altitude_m altitude_m in meters
|
||||
* @param builder StringBuilder to append
|
||||
* @param resId resource id of altitude string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeAltitude(double altitude_m, StringBuilder builder, int resId, String lineBreak) {
|
||||
void writeAltitude(double altitude_m, StringBuilder builder, int resId) {
|
||||
long altitudeInM = Math.round(altitude_m);
|
||||
long altitudeInFt = Math.round(Distance.of(altitude_m).toFT());
|
||||
builder.append(context.getString(resId, altitudeInM, altitudeInFt));
|
||||
builder.append(lineBreak);
|
||||
builder.append(TEXT_LINE_BREAK);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO Use null instead of Double.isInfinite
|
||||
//TODO Check that data ranges are valid (not less than zero etc.)
|
||||
//TODO Should be a Java record
|
||||
public class TrackStatistics {
|
||||
@@ -321,8 +320,8 @@ public class TrackStatistics {
|
||||
this.maxSpeed = maxSpeed;
|
||||
}
|
||||
|
||||
public boolean hasAltitudeMin() {
|
||||
return !Double.isInfinite(getMinAltitude());
|
||||
public boolean hasAltitudeExtremities() {
|
||||
return altitudeExtremities.hasData();
|
||||
}
|
||||
|
||||
public double getMinAltitude() {
|
||||
@@ -333,10 +332,6 @@ public class TrackStatistics {
|
||||
altitudeExtremities.setMin(altitude_m);
|
||||
}
|
||||
|
||||
public boolean hasAltitudeMax() {
|
||||
return !Double.isInfinite(getMaxAltitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum altitude.
|
||||
* This is calculated from the smoothed altitude, so this can actually be less than the current altitude.
|
||||
|
||||
@@ -78,9 +78,13 @@ public class TrackStatisticsUpdater {
|
||||
stats.merge(currentSegment);
|
||||
return stats;
|
||||
}
|
||||
public TrackStatistics getCurrentSegment() {
|
||||
// Take a snapshot - we don't want anyone messing with our currentSegment
|
||||
return new TrackStatistics(currentSegment);
|
||||
}
|
||||
|
||||
public void addTrackPoints(List<TrackPoint> trackPoints) {
|
||||
trackPoints.stream().forEachOrdered(this::addTrackPoint);
|
||||
trackPoints.forEach(this::addTrackPoint);
|
||||
}
|
||||
|
||||
public void addTrackPoint(TrackPoint trackPoint) {
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi
|
||||
|
||||
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||
if (trackIds != null && !trackIds.isEmpty()) {
|
||||
trackIds.stream().forEach(selection::addTrackId);
|
||||
trackIds.forEach(selection::addTrackId);
|
||||
}
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
@@ -103,7 +103,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.aggregated_statistics_filter) {
|
||||
ArrayList<FilterDialogFragment.FilterItem> filterItems = new ArrayList<>();
|
||||
adapter.getCategories().stream().forEach(activityType -> filterItems.add(new FilterDialogFragment.FilterItem(activityType, activityType, true)));
|
||||
adapter.getCategories().forEach(activityType -> filterItems.add(new FilterDialogFragment.FilterItem(activityType, activityType, true)));
|
||||
FilterDialogFragment.showDialog(getSupportFragmentManager(), filterItems);
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public class RecordingLayout implements Parcelable {
|
||||
|
||||
public RecordingLayout toRecordingLayout(boolean visibility) {
|
||||
RecordingLayout result = new RecordingLayout(this.getName());
|
||||
result.addFields(dataFields.stream().filter(f -> f.isVisible() == visibility).collect(Collectors.toList()));
|
||||
result.addFields(dataFields.stream().filter(f -> f.isVisible() == visibility).toList());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<Recycl
|
||||
Speed.zero()
|
||||
);
|
||||
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null);
|
||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, trackStatistics) ;
|
||||
}
|
||||
|
||||
private RecordingLayout recordingLayout;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
|
||||
private List<IntervalStatistics.Interval> intervalList;
|
||||
private final Context context;
|
||||
private final StackMode stackMode;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private UnitSystem unitSystem;
|
||||
private boolean isReportSpeed;
|
||||
|
||||
public IntervalStatisticsAdapter(Context context, StackMode stackMode, UnitSystem unitSystem, boolean isReportSpeed) {
|
||||
|
||||
@@ -51,7 +51,7 @@ public class IntervalsFragment extends Fragment {
|
||||
private IntervalListViewBinding viewBinding;
|
||||
|
||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
|
||||
if (PreferencesUtils.isKey(List.of(R.string.stats_units_key, R.string.stats_rate_key), key)) {
|
||||
updateIntervals(PreferencesUtils.getUnitSystem(), selectedInterval);
|
||||
if (intervalsAdapter != null) {
|
||||
intervalsAdapter.notifyDataSetChanged();
|
||||
@@ -114,7 +114,7 @@ public class IntervalsFragment extends Fragment {
|
||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
@@ -200,7 +200,7 @@ public class IntervalsFragment extends Fragment {
|
||||
|
||||
private void setIntervalsDropdownText() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.ui.util;
|
||||
import static android.content.Context.VIBRATOR_SERVICE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.view.Menu;
|
||||
@@ -18,11 +17,7 @@ public class ActivityUtils {
|
||||
|
||||
public static void vibrate(@NonNull Context context, Duration duration) {
|
||||
final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(duration.toMillis(), VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
} else {
|
||||
vibrator.vibrate(duration.toMillis());
|
||||
}
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(duration.toMillis(), VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,13 +49,13 @@ public class ListItemUtils {
|
||||
*/
|
||||
private static String getTimeDistance(String totalTime, String totalDistance) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (totalTime != null && totalTime.length() != 0) {
|
||||
if (totalTime != null && !totalTime.isEmpty()) {
|
||||
if (builder.length() != 0) {
|
||||
builder.append(" ‧ ");
|
||||
}
|
||||
builder.append(totalTime);
|
||||
}
|
||||
if (totalDistance != null && totalDistance.length() != 0) {
|
||||
if (totalDistance != null && !totalDistance.isEmpty()) {
|
||||
if (builder.length() != 0) {
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
@@ -48,8 +48,6 @@ abstract public class RecyclerViewSwipeDeleteCallback extends ItemTouchHelper.Ca
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
|
||||
View itemView = viewHolder.itemView;
|
||||
int itemHeight = itemView.getHeight();
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public class PermissionRequester {
|
||||
ArrayList<String> recording = new ArrayList<>(GPS_PERMISSION);
|
||||
recording.addAll(BLUETOOTH_PERMISSIONS);
|
||||
|
||||
RECORDING_PERMISSIONS = Collections.unmodifiableList(new ArrayList<>(recording));
|
||||
RECORDING_PERMISSIONS = List.copyOf(recording);
|
||||
|
||||
recording.addAll(NOTIFICATION_PERMISSIONS);
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class StatisticsUtils {
|
||||
|
||||
@Deprecated
|
||||
public static String emptyValue(@NonNull Context context, @NonNull String statTitle) {
|
||||
if (PreferencesUtils.isKey(R.string.stats_custom_layout_total_time_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_moving_time_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_pace_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_average_moving_pace_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_average_pace_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_fastest_pace_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_clock_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_time);
|
||||
} else if (PreferencesUtils.isKey(R.string.stats_custom_layout_distance_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_speed_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_average_speed_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_max_speed_key, statTitle) || PreferencesUtils.isKey(R.string.stats_custom_layout_average_moving_speed_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_float);
|
||||
} else if (PreferencesUtils.isKey(R.string.stats_custom_layout_coordinates_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_coordinates);
|
||||
} else {
|
||||
return context.getString(R.string.stats_empty_value_integer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ 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(0)
|
||||
.setDecimalPlaces(0)
|
||||
.setThreshold(Double.MAX_VALUE)
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
|
||||
+28
-11
@@ -34,7 +34,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
Pair<String, String> valueAndUnit = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext()).getDistanceParts(data.getTrackStatistics().getTotalDistance());
|
||||
.build(getContext()).getDistanceParts(data.getStatisticsTrack().getTotalDistance());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -46,7 +46,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
Pair<String, String> valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(data.getTrackStatistics().getTotalTime()), null);
|
||||
Pair<String, String> valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(data.getStatisticsTrack().getTotalTime()), null);
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -58,7 +58,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
String value = StringUtils.formatElapsedTime(data.getTrackStatistics().getMovingTime());
|
||||
String value = StringUtils.formatElapsedTime(data.getStatisticsTrack().getMovingTime());
|
||||
|
||||
getBinding().statsValue.setText(value);
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_moving_time));
|
||||
@@ -123,7 +123,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getAverageMovingSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getAverageMovingSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -140,7 +140,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getAverageSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getAverageSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -157,7 +157,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getMaxSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getMaxSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -174,7 +174,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getAverageMovingSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getAverageMovingSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -191,7 +191,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getAverageMovingSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getAverageMovingSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -199,6 +199,23 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
}
|
||||
}
|
||||
|
||||
public static class AverageSegmentPace extends GenericStatisticsViewHolder {
|
||||
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
SpeedFormatter speedFormatterSpeed = SpeedFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsCurrentSegment().getAverageMovingSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_average_segment_pace));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FastestPace extends GenericStatisticsViewHolder {
|
||||
|
||||
@Override
|
||||
@@ -208,7 +225,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(getContext());
|
||||
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getTrackStatistics().getMaxSpeed());
|
||||
Pair<String, String> valueAndUnit = speedFormatterSpeed.getSpeedParts(data.getStatisticsTrack().getMaxSpeed());
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -237,7 +254,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
|
||||
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), data.getTrackStatistics().getTotalAltitudeGain(), unitSystem);
|
||||
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), data.getStatisticsTrack().getTotalAltitudeGain(), unitSystem);
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
@@ -250,7 +267,7 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
|
||||
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), data.getTrackStatistics().getTotalAltitudeLoss(), unitSystem);
|
||||
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), data.getStatisticsTrack().getTotalAltitudeLoss(), unitSystem);
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
getBinding().statsUnit.setText(valueAndUnit.second);
|
||||
|
||||
@@ -24,6 +24,7 @@ public class Mapping {
|
||||
m.put(context.getString(R.string.stats_custom_layout_max_speed_key), GenericStatisticsViewHolder.MaxSpeed::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_average_moving_pace_key), GenericStatisticsViewHolder.AverageMovingPace::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_average_pace_key), GenericStatisticsViewHolder.AveragePace::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_average_segment_pace_key), GenericStatisticsViewHolder.AverageSegmentPace::new);
|
||||
m.put(context.getString(R.string.stats_custom_layout_fastest_pace_key), GenericStatisticsViewHolder.FastestPace::new);
|
||||
|
||||
m.put(context.getString(R.string.stats_custom_layout_altitude_key), GenericStatisticsViewHolder.Altitude::new);
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
android:translateY="24">
|
||||
<path
|
||||
android:pathData="M85.133,20.59C85.731,18.546 84.756,17.817 82.965,18.968L23.874,56.967c-1.791,1.152 -1.519,2.224 0.604,2.382L49.019,61.18c2.124,0.159 4.477,1.918 5.229,3.91l8.7,23.021c0.753,1.992 1.857,1.949 2.455,-0.094z"
|
||||
android:fillColor="#FFFFFF"></path>
|
||||
android:fillColor="#FFFFFF" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
android:id="@+id/recycler_view_visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@id/recycler_view_not_visible"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
android:paddingTop="?attr/actionBarSize"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="?attr/actionBarSize">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/custom_layout_list_add_linear_layout"
|
||||
@@ -82,7 +82,8 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
android:id="@+id/aggregated_stats_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
@@ -35,8 +36,8 @@
|
||||
style="?attr/textAppearanceHeadline4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:padding="16dp"
|
||||
android:text="@string/aggregated_stats_empty_message" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -17,22 +17,22 @@
|
||||
app:navigationIcon="@drawable/ic_baseline_import_export_24" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="@dimen/m3_bottom_nav_min_height"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_vertical_half"
|
||||
@@ -43,22 +43,22 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_done"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="48dp"
|
||||
android:text="@string/value_unknown"
|
||||
app:layout_constraintEnd_toStartOf="@id/import_progress_slash"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="1" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_slash"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:text="@string/generic_separator_done_total"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_done"
|
||||
app:layout_constraintEnd_toStartOf="@id/import_progress_total"
|
||||
@@ -67,15 +67,15 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_total"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/value_unknown"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_slash"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/import_progress_slash"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_slash"
|
||||
tools:text="10" />
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_slash" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/import_progress_bar"
|
||||
@@ -93,11 +93,11 @@
|
||||
<!-- Files imported correctly -->
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_ok_msg"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:gravity="end"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/import_progress_summary_ok_msg"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -105,14 +105,14 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_ok"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_summary_ok_msg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_summary_ok_msg"
|
||||
tools:text="10" />
|
||||
android:text="@string/value_unknown" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/import_progress_summary_ok_barrier"
|
||||
@@ -126,17 +126,17 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="import_progress_summary_ok_msg,import_progress_summary_ok" />
|
||||
app:constraint_referenced_ids="import_progress_summary_ok_msg,import_progress_summary_ok"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Already exists files -->
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_exists_msg"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:gravity="end"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/import_progress_summary_exists_msg"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -144,14 +144,14 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_exists"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_summary_exists_msg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_summary_exists_msg"
|
||||
tools:text="10" />
|
||||
android:text="@string/value_unknown" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/import_progress_summary_exists_barrier"
|
||||
@@ -165,17 +165,17 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="import_progress_summary_exists_msg,import_progress_summary_exists" />
|
||||
app:constraint_referenced_ids="import_progress_summary_exists_msg,import_progress_summary_exists"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Importing error files -->
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_errors_msg"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:gravity="end"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="end"
|
||||
android:text="@string/import_progress_summary_errors_msg"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -183,14 +183,14 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary_errors"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_summary_errors_msg"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_vertical_half"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_summary_errors_msg"
|
||||
tools:text="10" />
|
||||
android:text="@string/value_unknown" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/import_progress_summary_errors_barrier"
|
||||
@@ -204,8 +204,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:constraint_referenced_ids="import_progress_summary_errors_msg,import_progress_summary_errors" />
|
||||
app:constraint_referenced_ids="import_progress_summary_errors_msg,import_progress_summary_errors"
|
||||
tools:visibility="visible" />
|
||||
<!-- End Summary -->
|
||||
|
||||
<ImageView
|
||||
@@ -214,18 +214,18 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_alert_msg"
|
||||
app:layout_constraintEnd_toStartOf="@id/import_progress_alert_msg"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_alert_msg"
|
||||
app:srcCompat="@drawable/ic_report_problem_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_alert_msg"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
style="?attr/textAppearanceSubtitle1"
|
||||
android:layout_marginTop="48dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
@@ -258,7 +258,7 @@
|
||||
android:layout_gravity="end"
|
||||
android:text="@android:string/ok"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/intervals_input_layout"
|
||||
@@ -29,26 +29,26 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_distance"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/stats_distance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_rate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
tools:text="Speed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_altitude"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryHeader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/stats_gain_loss" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -64,13 +64,14 @@
|
||||
android:id="@+id/interval_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp" />
|
||||
android:layout_margin="8dp"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_list_empty_view"
|
||||
style="?attr/textAppearanceHeadline4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
style="?attr/textAppearanceHeadline4"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/interval_list_empty_message" />
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ limitations under the License.
|
||||
android:id="@+id/marker_list_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_marker_show_24dp"
|
||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior"
|
||||
app:menu="@menu/marker_list" />
|
||||
app:menu="@menu/marker_list"
|
||||
app:navigationIcon="@drawable/ic_marker_show_24dp" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.search.SearchView
|
||||
@@ -51,7 +51,8 @@ limitations under the License.
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/marker_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/marker_list_empty"
|
||||
|
||||
@@ -46,6 +46,7 @@ limitations under the License.
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="?attr/actionBarSize"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
|
||||
@@ -553,4 +553,56 @@ limitations under the License.
|
||||
<string name="gps_recording_without_signal">OpenTracks optager uden GPS-data</string>
|
||||
<string name="value_int_seconds">%1$d s (anbefalet)</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_title">Filtrer Bluetooth-enheder</string>
|
||||
</resources>
|
||||
<string name="settings_recording_show_gps_disabled_warning_summary">Viser en advarsel under registrering hvis GPS er deaktiveret.</string>
|
||||
<string name="settings_locale_title">Sprog</string>
|
||||
<string name="stats_gain_loss">Vundet & Tabt</string>
|
||||
<string name="voiceHoursPlural">{n, plural, =1 {1 time} other {{n,number} timer} }</string>
|
||||
<string name="show_on_dashboard_not_installed">Ingen applikation der understøtter OpenTracks Dashboard API installeret.</string>
|
||||
<string name="permission_recording_failed">OpenTracks kræver adgang til din lokation såvel som enheder i nærheden.</string>
|
||||
<string name="create_marker_error">Markør kan ikke oprettes. Prøv igen.</string>
|
||||
<string name="settings_recording_min_sampling_interval_title">Tidsinterval mellem samplinger</string>
|
||||
<string name="settings_night_mode_option_day_dynamic">Dag (DynamicColor)</string>
|
||||
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {1 kilometer i timen} other {{n,number,#.0} kilometre i timen} }</string>
|
||||
<string name="empty">tom</string>
|
||||
<string name="settings_chart_display_elevation_title">Vis højde</string>
|
||||
<string name="settings_chart_display_pace_or_speed_title">Vis tempo eller hastighed</string>
|
||||
<string name="settings_chart_display_heart_rate_title">Vis puls</string>
|
||||
<string name="settings_charts_title">Grafer</string>
|
||||
<string name="settings_theme_switch_restart">Tema-ændringer kræver muligvis en manuel genstart.</string>
|
||||
<string name="settings_recording_show_gps_disabled_warning_title">Advar om deaktiveret GPS</string>
|
||||
<string name="settings_locale_system_default">Systemstandard</string>
|
||||
<string name="settings_night_mode_option_night_dynamic">Nat (DynamicColor)</string>
|
||||
<string name="settings_night_mode_option_night_oled">OLED-venlig</string>
|
||||
<string name="settings_night_mode_option_night_oled_dynamic">OLED-venlig (DynamicColor)</string>
|
||||
<string name="settings_announcements_unit">Distance/hastigheds-enhed</string>
|
||||
<string name="track_edit_time_offset">Tidszone-forskydning</string>
|
||||
<string name="value_internal_sensor">Intern sensor</string>
|
||||
<string name="voiceMinutesPlural">{n, plural, =1 {1 minut} other {{n,number} minutter} }</string>
|
||||
<string name="voiceSecondsPlural">{n, plural, =1 {1 sekund} other {{n,number} sekunder} }</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 mil i timer} other {{n,number,#.0} mil i timen} }</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 knob} other {{n,number,#.0} knob} }</string>
|
||||
<string name="voice_on_device_speaker_title">Brug enhedens højttaler</string>
|
||||
<string name="voice_per_mile">per mil</string>
|
||||
<string name="voice_per_nautical_mile">per sømil</string>
|
||||
<string name="lap_time">Omgangstid</string>
|
||||
<string name="pace">Tempo</string>
|
||||
<string name="lap_speed">Omgangshastighed</string>
|
||||
<string name="speed">Gennemsnitlig bevægelseshastighed</string>
|
||||
<string name="total_distance">Samlet distance</string>
|
||||
<string name="voiceDistanceMilesPlural">{n, plural, =1 {1 mil} other {{n,number,#.0} mil} }</string>
|
||||
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 sømil} other {{n,number,#.0} sømil} }</string>
|
||||
<string name="average_heart_rate">Gennemsnitspuls</string>
|
||||
<string name="current_heart_rate">Aktuel puls</string>
|
||||
<string name="lap_heart_rate">Omgangspuls</string>
|
||||
<string name="no_compatible_camera_installed">Ingen kompatibel kameraapplikation installeret</string>
|
||||
<string name="no_compatible_gallery_installed">Ingen kompatibel billedgalleriapplikation installeret</string>
|
||||
<string name="error_crash_title">%1$s gik ned</string>
|
||||
<string name="share_crash_report">Del fejlrapport</string>
|
||||
<string name="report_bug">Rapporter fejl</string>
|
||||
<string name="always">Altid</string>
|
||||
<string name="introduction_osm_dashboard">OpenTracks indeholder ikke selv et kort. Installer venligst OSMDashboard for at se registreringer på et kort.</string>
|
||||
<string name="settings_recording_location_distance_summary">%1$s mellem registrerede lokationer</string>
|
||||
<string name="settings_announcements_lap_power">Omgangseffekt</string>
|
||||
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watt} }</string>
|
||||
<string name="power">Effekt</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
@@ -19,16 +19,11 @@ limitations under the License.
|
||||
<string name="about_url">Pagina web del progetto: <a href="%1$s">%1$s</a></string>
|
||||
<string name="about_version_name">Nome versione: %1$s</string>
|
||||
<string name="about_version_code">Codice versione: %1$d</string>
|
||||
<string name="about_history">OpenTracks è un fork di <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (cessato nel 2016).</string>
|
||||
<string name="about_history">OpenTracks è un fork di <a href="https://code.google.com/archive/p/mytracks/"><i>MyTracks di Google</i></a> (cessato nel 2016).</string>
|
||||
<string name="about_libraries">Librerie</string>
|
||||
<!-- Help -->
|
||||
<string name="help_feature_title">OpenTracks ha le seguenti caratteristiche:</string>
|
||||
<string name="help_feature_content">* crea tracce GPS
|
||||
\n * utilizza un sensore di frequenza cardiaca (solo Bluetooth LE)
|
||||
\n * comunicazioni vocali (per tempo o distanza)
|
||||
\n * crea marcatori (incl. immagini)
|
||||
\n * funziona completamente offline
|
||||
\n * mostra i tracce su una mappa (vedi sotto)</string>
|
||||
<string name="help_feature_content">* crea tracce GPS \n * utilizza un sensore di frequenza cardiaca (solo Bluetooth LE) \n * comunicazioni vocali (per tempo o distanza) \n * crea marcatori (comprese le immagini) \n * funziona completamente offline \n * mostra i tracciati su una mappa (vedi sotto)</string>
|
||||
<string name="help_track_map_title">Come visualizzare una traccia su una mappa\?</string>
|
||||
<string name="help_track_map_content">
|
||||
Utilizzare la piccola icona della mappa.\n
|
||||
@@ -371,6 +366,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="export_track_already_exists_msg">Il file %1$s esiste già nella cartella di salvataggio.</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">Completato con un errore</item>
|
||||
<item quantity="many">Completato con %1$d errori</item>
|
||||
<item quantity="other">Completato con %1$d errori</item>
|
||||
</plurals>
|
||||
<string name="generic_click_twice_cancel">Premi il tasto indietro di nuovo per annullare</string>
|
||||
@@ -437,7 +433,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="track_distance_notification">Distanza: %1$s</string>
|
||||
<string name="track_speed_notification">Velocità: %1$s</string>
|
||||
<string name="track_pace_notification">Ritmo: %1$s</string>
|
||||
<string name="about_third_party_data">Dati di terzi</string>
|
||||
<string name="about_third_party_data">Dati di terze parti</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>
|
||||
@@ -610,4 +606,19 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
|
||||
<string name="lap_heart_rate">Frequenza cardiaca del giro</string>
|
||||
<string name="empty">vuoto</string>
|
||||
<string name="settings_recording_show_gps_disabled_warning_summary">Durante la registrazione, mostra un avviso se il GPS è disabilitato.</string>
|
||||
<string name="settings_night_mode_option_day_dynamic">Giorno (colore dinamico)</string>
|
||||
<string name="track_edit_time_offset">Compensazione fuso orario</string>
|
||||
<string name="marker_detail_title">%1$d di %2$d: %3$s</string>
|
||||
<string name="activity_type_workout">allenamento</string>
|
||||
<string name="settings_charts_title">Grafici</string>
|
||||
<string name="settings_chart_display_elevation_title">Visualizza l\'elevazione</string>
|
||||
<string name="settings_chart_display_pace_or_speed_title">Visualizza il ritmo o la velocità</string>
|
||||
<string name="settings_chart_display_heart_rate_title">Visualizza la frequenza cardiaca</string>
|
||||
<string name="settings_recording_show_gps_disabled_warning_title">Avviso di GPS disabilitato</string>
|
||||
<string name="settings_night_mode_option_system_dynamic">Sistema (colore dinamico)</string>
|
||||
<string name="settings_night_mode_option_night_dynamic">Notte (colore dinamico)</string>
|
||||
<string name="settings_night_mode_option_night_oled_dynamic">Adatto per OLED (colore dinamico)</string>
|
||||
<string name="voiceIdle">Passaggio a inattivo.</string>
|
||||
</resources>
|
||||
|
||||
@@ -38,7 +38,7 @@ limitations under the License.
|
||||
<string name="activity_type_horse_back_riding">верхова їзда на конях</string>
|
||||
<string name="activity_type_hot_air_balloon">повітряна куля</string>
|
||||
<string name="activity_type_ice_sailing">буерний спорт</string>
|
||||
<string name="activity_type_inline_skating">катання на роликових ковзанах з колесами, розташованими в лінію</string>
|
||||
<string name="activity_type_inline_skating">роликові ковзани</string>
|
||||
<string name="activity_type_kayaking">каякінг</string>
|
||||
<string name="activity_type_kite_surfing">кайтсерфінг</string>
|
||||
<string name="activity_type_land_sailing">наземні вітрильники</string>
|
||||
@@ -52,7 +52,7 @@ limitations under the License.
|
||||
<string name="activity_type_para_gliding">парапланеризм</string>
|
||||
<string name="activity_type_rc_airplane">авіамодель на дистанційному керуванні</string>
|
||||
<string name="activity_type_rc_boat">човен на дистанційному керуванні</string>
|
||||
<string name="activity_type_rc_helicopter">Вертоліт на дистанційному керуванні</string>
|
||||
<string name="activity_type_rc_helicopter">вертоліт на дистанційному керуванні</string>
|
||||
<string name="activity_type_riding">верхова їзда</string>
|
||||
<string name="activity_type_road_biking">шляховий велоспорт</string>
|
||||
<string name="activity_type_roller_skiing">роликові лижі</string>
|
||||
@@ -83,29 +83,29 @@ limitations under the License.
|
||||
<string name="activity_type_wind_surfing">віндсерфінг</string>
|
||||
<!-- Description -->
|
||||
<string name="description_activity_type">Тип активності: %1$s</string>
|
||||
<string name="description_average_moving_pace_in_minute">Середній темп руху: %1$s хв/км (%2$s хв/мл)</string>
|
||||
<string name="description_average_moving_speed">Середня швидкість руху: %1$.2f км/год (%2$.1f мл/год)</string>
|
||||
<string name="description_average_pace_in_minute">Середній темп: %1$s хв/км (%2$s хв/мл)</string>
|
||||
<string name="description_average_speed">Середня швидкість: %1$.2f км/год (%2$.1f мл/год)</string>
|
||||
<string name="description_altitude_gain">Збільшення висоти: %1$d м (%2$d фт)</string>
|
||||
<string name="description_average_moving_pace_in_minute">Середній темп руху: %1$s хв/км (%2$s хв/милю)</string>
|
||||
<string name="description_average_moving_speed">Середня швидкість руху: %1$.2f км/год (%2$.1f миль/год)</string>
|
||||
<string name="description_average_pace_in_minute">Середній темп: %1$s хв/км (%2$s хв/милю)</string>
|
||||
<string name="description_average_speed">Середня швидкість: %1$.2f км/год (%2$.1f миль/год)</string>
|
||||
<string name="description_altitude_gain">Збільшення висоти: %1$d м (%2$d фут)</string>
|
||||
<string name="description_altitude_imperial">Висота (фути)</string>
|
||||
<string name="description_altitude_metric">Висота (метри)</string>
|
||||
<string name="description_fastest_pace_in_minute">Найшвидший темп: %1$s хв/км (%2$s хв/мл)</string>
|
||||
<string name="description_max_altitude">Максимальний підйом: %1$d м (%2$d фт)</string>
|
||||
<string name="description_max_speed">Макс. швидкість: %1$.2f км/год (%2$.1f мл/год)</string>
|
||||
<string name="description_min_altitude">Мін. підйом: %1$d м (%2$d фт)</string>
|
||||
<string name="description_fastest_pace_in_minute">Найшвидший темп: %1$s хв/км (%2$s хв/милю)</string>
|
||||
<string name="description_max_altitude">Максимальний підйом: %1$d м (%2$d фут)</string>
|
||||
<string name="description_max_speed">Макс. швидкість: %1$.2f км/год (%2$.1f миль/год)</string>
|
||||
<string name="description_min_altitude">Мін. підйом: %1$d м (%2$d фут)</string>
|
||||
<string name="description_moving_time">Час руху: %1$s</string>
|
||||
<string name="description_pace_imperial">Темп (хв/мл)</string>
|
||||
<string name="description_pace_imperial">Темп (хв/милю)</string>
|
||||
<string name="description_pace_metric">Темп (хв/км)</string>
|
||||
<string name="description_recorded_time">Записано: %1$s</string>
|
||||
<string name="description_speed_ms">Швидкість (м/с)</string>
|
||||
<string name="description_sensor_cadence">Темп (об/хв)</string>
|
||||
<string name="description_sensor_heart_rate">Пульс (уд/хв)</string>
|
||||
<string name="description_sensor_cadence">Темп (об./хв)</string>
|
||||
<string name="description_sensor_heart_rate">Пульс (уд./хв)</string>
|
||||
<string name="description_sensor_power">Потужність (Вт)</string>
|
||||
<string name="description_speed_imperial">Швидкість (миль/год.)</string>
|
||||
<string name="description_speed_metric">Швидкість (км/год.)</string>
|
||||
<string name="description_speed_imperial">Швидкість (миль/год)</string>
|
||||
<string name="description_speed_metric">Швидкість (км/год)</string>
|
||||
<string name="description_time">Час</string>
|
||||
<string name="description_total_distance">Загальна відстань: %1$.2f км (%2$.1f мл)</string>
|
||||
<string name="description_total_distance">Загальна відстань: %1$.2f км (%2$.1f миль)</string>
|
||||
<string name="description_total_time">Загальний час: %1$s</string>
|
||||
<!-- Export -->
|
||||
<string name="export_progress_message">Триває експорт у %1$s…</string>
|
||||
@@ -117,7 +117,7 @@ limitations under the License.
|
||||
<string name="generic_description_line">Опис: %1$s</string>
|
||||
<string name="generic_name">Назва</string>
|
||||
<string name="generic_name_line">Назва: %1$s</string>
|
||||
<string name="generic_recording">Записування…</string>
|
||||
<string name="generic_recording">Запис…</string>
|
||||
<string name="generic_save">Зберегти</string>
|
||||
<string name="generic_tracks">Шляхи</string>
|
||||
<!-- Gps -->
|
||||
@@ -132,11 +132,11 @@ limitations under the License.
|
||||
<string name="image_stop">Зупинити</string>
|
||||
<string name="image_track">Трек</string>
|
||||
<!-- Import -->
|
||||
<string name="import_progress_message">Імпорт із каталогу %1$s…</string>
|
||||
<string name="import_progress_message">Імпорт із %1$s…</string>
|
||||
<!-- Marker -->
|
||||
<string name="marker_add_error">Неможливо вставити маркер, оскільки відсутній сигнал GPS. Повторіть спробу.</string>
|
||||
<string name="marker_add_success">Маркер вставлено</string>
|
||||
<string name="marker_add_canceled">Помилка: не додано маркер</string>
|
||||
<string name="marker_add_canceled">Скасовано, не додано маркер</string>
|
||||
<string name="marker_delete_one_confirm_message">Маркер буде назавжди видалено з пам’яті пристрою</string>
|
||||
<string name="marker_delete_one_confirm_title">Видалити маркер?</string>
|
||||
<string name="marker_delete_multiple_confirm_message">Вибрані маркери буде назавжди видалено з пам’яті пристрою</string>
|
||||
@@ -166,14 +166,14 @@ limitations under the License.
|
||||
<!-- Search -->
|
||||
<!-- Sensor State -->
|
||||
<string name="sensor_state_heart_rate">Пульс</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d уд/хв</string>
|
||||
<string name="sensor_state_heart_rate_value">%1$d уд./хв</string>
|
||||
<string name="sensor_state_power">Потужність</string>
|
||||
<!-- Settings Advanced -->
|
||||
<string name="settings_advanced">Розширені</string>
|
||||
<!-- Settings Chart -->
|
||||
<string name="settings_chart_by_distance">Дистанція</string>
|
||||
<string name="settings_chart_by_time">Час</string>
|
||||
<string name="settings_recording_default_activity_title">Тип за умовчанням</string>
|
||||
<string name="settings_recording_default_activity_title">Тип за замовчуванням</string>
|
||||
<string name="settings_recording_location_frequency_summary">%1$s між перевірками місцезнаходження</string>
|
||||
<string name="settings_recording_max_recording_distance_summary">Кожні %1$s шляху відміряється новий відрізок</string>
|
||||
<string name="settings_recording_max_recording_distance_title">Максимальна відстань</string>
|
||||
@@ -183,9 +183,9 @@ limitations under the License.
|
||||
<string name="settings_recording_track_name_date_iso_8601_option">Дата (ISO 8601)</string>
|
||||
<string name="settings_recording_track_name_date_local_option">Дата (локаль. формат)</string>
|
||||
<string name="settings_recording_track_name_number_option">Номер</string>
|
||||
<string name="settings_recording_track_name_title">Назва шляху за умовчанням</string>
|
||||
<string name="settings_recording_track_name_title">Назва шляху за замовчуванням</string>
|
||||
<!-- Settings Reset -->
|
||||
<string name="settings_reset_confirm_message">Усі налаштування буде скинуто до значень за умовчанням.</string>
|
||||
<string name="settings_reset_confirm_message">Усі налаштування буде скинуто до значень за замовчуванням. Це не призведе до видалення жодних треків з пристрою.</string>
|
||||
<string name="settings_reset_confirm_title">Скинути зараз?</string>
|
||||
<string name="settings_reset_done">Для всіх налаштувань відновлено значення за умовчанням</string>
|
||||
<!-- Settings Sensor -->
|
||||
@@ -231,14 +231,14 @@ limitations under the License.
|
||||
<!-- Track Widget -->
|
||||
<!-- Tracks -->
|
||||
<!-- Unit -->
|
||||
<string name="unit_feet">фут.</string>
|
||||
<string name="unit_feet">фути</string>
|
||||
<string name="unit_kilometer">км</string>
|
||||
<string name="unit_kilometer_per_hour">км/год.</string>
|
||||
<string name="unit_kilometer_per_hour">км/год</string>
|
||||
<string name="unit_meter">м</string>
|
||||
<string name="unit_mile">милі</string>
|
||||
<string name="unit_mile_per_hour">миль на годину</string>
|
||||
<string name="unit_minute_per_kilometer">хв/км</string>
|
||||
<string name="unit_minute_per_mile">хв./милю</string>
|
||||
<string name="unit_minute_per_mile">хв/милю</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet_excellent_gps">%1$d фут. (відмінний сигнал GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d фут. (рекомендовано)</string>
|
||||
@@ -248,7 +248,7 @@ limitations under the License.
|
||||
<string name="value_integer_meter_recommended">%1$d м (рекомендовано)</string>
|
||||
<string name="value_integer_mile_hour">%1$d миль/год</string>
|
||||
<string name="value_integer_minute">%1$d хв</string>
|
||||
<string name="value_integer_second">%1$d сек</string>
|
||||
<string name="value_integer_second">%1$d с</string>
|
||||
<string name="value_none">Немає</string>
|
||||
<string name="value_off">Вимк.</string>
|
||||
<string name="value_smallest_recommended">Найменше (рекомендовано)</string>
|
||||
@@ -261,7 +261,7 @@ limitations under the License.
|
||||
<string name="waypoint_type_bus_stop">автобусна зупинка</string>
|
||||
<string name="waypoint_type_cafe">кафе</string>
|
||||
<string name="waypoint_type_campfire">вогнище</string>
|
||||
<string name="waypoint_type_campground">наметові табори</string>
|
||||
<string name="waypoint_type_campground">наметовий табір</string>
|
||||
<string name="waypoint_type_church">церква</string>
|
||||
<string name="waypoint_type_coffee_house">кав’ярня</string>
|
||||
<string name="waypoint_type_convenience_store">цілодобова крамниця</string>
|
||||
@@ -286,7 +286,7 @@ limitations under the License.
|
||||
<string name="waypoint_type_meeting_point">місце зустрічі</string>
|
||||
<string name="waypoint_type_opera">опера</string>
|
||||
<string name="waypoint_type_parking">паркування</string>
|
||||
<string name="waypoint_type_police_station">міліцейський відділок</string>
|
||||
<string name="waypoint_type_police_station">поліцейський відділок</string>
|
||||
<string name="waypoint_type_post_office">поштове відділення</string>
|
||||
<string name="waypoint_type_pothole">вибоїна</string>
|
||||
<string name="waypoint_type_railway_crossing">залізничний переїзд</string>
|
||||
@@ -342,30 +342,30 @@ limitations under the License.
|
||||
<string name="open_track_as_trackfileformat">Відкрити як %1$s</string>
|
||||
<string name="activity_type_escooter">електро-байк</string>
|
||||
<string name="settings_sensor_wheel_circumference">Окружність колеса (мм)</string>
|
||||
<string name="description_altitude_loss">Зменшення висоти: %1$d м (%2$d фут.)</string>
|
||||
<string name="description_altitude_loss">Зменшення висоти: %1$d м (%2$d фут)</string>
|
||||
<string name="export_progress_summary_new_msg">Нові файли експортовано</string>
|
||||
<string name="export_progress_summary_overwrite_msg">Файли перезаписано</string>
|
||||
<string name="export_progress_summary_skip_msg">Пропущені файли</string>
|
||||
<string name="export_progress_summary_errors_msg">Помилки</string>
|
||||
<string name="generic_open_track">Відкрити Трек</string>
|
||||
<string name="generic_open_track">Відкрити трек</string>
|
||||
<string name="import_file_imported">Файл %1$s імпортовано</string>
|
||||
<string name="import_prevent_reimport">Ви встановили заборону на повторне імпортування треку</string>
|
||||
<string name="import_progress_summary_ok_msg">Файли імпортовано</string>
|
||||
<string name="import_progress_summary_exists_msg">Вже існує</string>
|
||||
<string name="import_progress_summary_errors_msg">Помилки</string>
|
||||
<string name="export_with_photos">з фото</string>
|
||||
<string name="export_without_photos">без</string>
|
||||
<string name="export_track_already_exists_msg">Файл %1$s вже існує в цільовій директорії.</string>
|
||||
<string name="export_all_do_it_for_all">робити це для усіх конфліктів</string>
|
||||
<string name="export_without_photos">без фото</string>
|
||||
<string name="export_track_already_exists_msg">Файл %1$s уже існує в цільовому каталозі.</string>
|
||||
<string name="export_all_do_it_for_all">робити це для всіх конфліктів</string>
|
||||
<string name="export_track_errors">Файли не експортовано</string>
|
||||
<string name="generic_overwrite">Перезаписати</string>
|
||||
<string name="generic_skip">Пропустити</string>
|
||||
<string name="generic_show_errors">Показати помилки</string>
|
||||
<string name="generic_completed">Завершено</string>
|
||||
<string name="generic_click_twice_cancel">Натисніть ще раз щоб відмінити</string>
|
||||
<string name="generic_click_twice_cancel">Натисніть ще раз, щоб скасувати</string>
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="gps_disabled_msg">GPS вимкнено</string>
|
||||
<string name="import_thread_interrupted">Поток перервано</string>
|
||||
<string name="import_thread_interrupted">Потік перервано</string>
|
||||
<string name="import_unsupported_format">Формат файлу не підтримується</string>
|
||||
<string name="import_parser_error">Помилка парсера: %1$s</string>
|
||||
<string name="import_no_kml_file_found">Файл kml не знайдено в KMZ</string>
|
||||
@@ -374,7 +374,7 @@ limitations under the License.
|
||||
<string name="marker_add_photo_canceled">Скасовано, фото-маркування не додано</string>
|
||||
<string name="menu_insert_gallery_img">Додати зображення з галереї</string>
|
||||
<string name="menu_resume_track">Відновити трек</string>
|
||||
<string name="permission_gps_failed">OpenTracks вимагає дозволу на використання GPS.</string>
|
||||
<string name="permission_gps_failed">OpenTracks потребує дозволу на використання GPS.</string>
|
||||
<string name="settings_night_mode_option_system">Системна</string>
|
||||
<string name="about_description"><i>OpenTracks</i> - це застосунок для відстеження спортивної діяльності, який поважає вашу приватність.</string>
|
||||
<string name="about_history">OpenTracks це відгалуження застосунку <a href="https://code.google.com/archive/p/mytracks/"><i>Google\'s MyTracks</i></a> (закритого в 2016р).</string>
|
||||
@@ -399,9 +399,9 @@ limitations under the License.
|
||||
<string name="export_error_post_workout">Не вдалося виконати експорт після тренування, перевірте каталог експорту.</string>
|
||||
<string name="aggregated_stats_empty_message">Запишіть свій перший трек, щоб побачити загальну статистику</string>
|
||||
<string name="gps_fixed_and_ready">GPS готовий до роботи</string>
|
||||
<string name="sensor_state_cadence_avg">Сер. каданс</string>
|
||||
<string name="sensor_state_cadence_avg">Сер. каденс</string>
|
||||
<string name="import_unable_to_import_file">Неможливо імпортувати файл: %1$s</string>
|
||||
<string name="sensor_state_cadence_max">Макс. каданс</string>
|
||||
<string name="sensor_state_cadence_max">Макс. каденс</string>
|
||||
<string name="sensor_could_not_scan">Не вдалося знайти Bluetooth пристрій (помилка %1$i).</string>
|
||||
<string name="bluetooth_disabled">Увімкніть Bluetooth.</string>
|
||||
<string name="settings_default_trackfileformat">Формат файла експорту/обміну</string>
|
||||
@@ -415,8 +415,8 @@ limitations under the License.
|
||||
\n b) Через звичайний картографічний застосунок (на приклад, <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="settings_prevent_reimport_tracks_title">Заборонити повторний імпорт треків</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">Завершено з %1$d однією помилкою</item>
|
||||
<item quantity="few">Заповнено з %1$d помилками</item>
|
||||
<item quantity="one">Завершено з %1$d помилкою</item>
|
||||
<item quantity="few">Завершено з %1$d помилками</item>
|
||||
<item quantity="many">Завершено з %1$d помилками</item>
|
||||
<item quantity="other">Завершено з %1$d помилками</item>
|
||||
</plurals>
|
||||
@@ -425,22 +425,22 @@ limitations under the License.
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Екран завжди включено</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_title">Повний екран</string>
|
||||
<string name="settings_recording_fullscreen_on_while_recording_summary">Під час запису бути в повноекранному режимі.</string>
|
||||
<string name="hold_to_stop">Затисніть щоб зупинити</string>
|
||||
<string name="hold_to_stop">Затисніть, щоб зупинити</string>
|
||||
<string name="track_distance_notification">Відстань: %1$s</string>
|
||||
<string name="track_speed_notification">Швидкість: %1$s</string>
|
||||
<string name="track_pace_notification">Темп: %1$s</string>
|
||||
<string name="about_third_party_data">Дані третіх сторін</string>
|
||||
<string name="track_delete_not_recording">Цей трек не може бути видалений в тому вигляді, в якому він записаний в даний момент.</string>
|
||||
<string name="track_delete_not_recording">Цей трек неможливо видалити, оскільки він зараз записується.</string>
|
||||
<string name="menu_voice_rate">Швидкість передачі голосу</string>
|
||||
<string name="settings_sensor_cycling_cadence">Велоспорт: частота обертання педалей</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Їзда на велосипеді: відстань і швидкість</string>
|
||||
<string name="settings_sensor_running_speed_and_cadence">БІГ: дистанція, швидкість і частота</string>
|
||||
<string name="settings_sensor_cycling_cadence">Каденс</string>
|
||||
<string name="settings_sensor_cycling_distance_speed">Відстань і швидкість</string>
|
||||
<string name="settings_sensor_running_speed_and_cadence">Дистанція, швидкість і каденс</string>
|
||||
<string name="generic_choose_an_option">Оберіть опцію</string>
|
||||
<string name="permission_bluetooth_failed">OpenTracks просить дозволу використання Bluetooth.</string>
|
||||
<string name="permission_bluetooth_failed">OpenTracks потребує дозволу на використання Bluetooth.</string>
|
||||
<string name="permission_bluetooth_failed_rejected">Вам необхідно вручну надати OpenTracks доступ до прилеглих пристроїв в системних налаштуваннях.</string>
|
||||
<string name="field_set_primary">Первинний</string>
|
||||
<string name="field_remove_from_layout">Видаляти</string>
|
||||
<string name="stats_clock">Години</string>
|
||||
<string name="field_remove_from_layout">Видалити</string>
|
||||
<string name="stats_clock">Годинник</string>
|
||||
<string name="settings_recording_customize_layout_select_columns">Поля даних у рядку</string>
|
||||
<string name="settings_export_section">Експорт</string>
|
||||
<string name="field_set_secondary">Вторинний</string>
|
||||
@@ -449,57 +449,57 @@ limitations under the License.
|
||||
<string name="generic_from">Від</string>
|
||||
<string name="generic_to">До</string>
|
||||
<string name="menu_select_layout">Виберіть макет</string>
|
||||
<string name="menu_add_custom_layout">Нове Планування</string>
|
||||
<string name="menu_add_custom_layout">Нове планування</string>
|
||||
<string name="menu_filter">Фільтр</string>
|
||||
<string name="settings_reset_summary">Скинути налаштування до значень за замовчуванням</string>
|
||||
<string name="settings_activity_defaults_category">Дії за замовчуванням</string>
|
||||
<string name="settings_units_category">Одиниця</string>
|
||||
<string name="settings_import_export_summary">Імпорт, Експорт, Автоэкспорт</string>
|
||||
<string name="settings_announcements_summary">Час, відстань, швидкість передачі голосу</string>
|
||||
<string name="settings_units_category">Одиниці</string>
|
||||
<string name="settings_import_export_summary">Імпорт, Експорт, Автоекспорт</string>
|
||||
<string name="settings_announcements_summary">Час, Відстань, Швидкість передачі голосу</string>
|
||||
<string name="settings_import_export_title">Імпорт та експорт</string>
|
||||
<string name="settings_recording_customize_layout_title">Налаштуйте свій макет запису</string>
|
||||
<string name="stats_custom_layout_default_layout">Макет за замовчуванням</string>
|
||||
<string name="custom_layout_select_layout">Виберіть макет</string>
|
||||
<string name="custom_layout_list_layout_removed">Макет був видалений</string>
|
||||
<string name="custom_layout_list_layout_removed">Макет видалено</string>
|
||||
<string name="custom_layout_list_edit_already_exists">Ім\'я макета вже існує</string>
|
||||
<string name="custom_layout_list_edit_name">Введіть ім\'я макета для додавання</string>
|
||||
<string name="settings_announcements_distance_frequency">Довжина кола</string>
|
||||
<string name="settings_announcements_totaltime_frequency">Часовий інтервал</string>
|
||||
<string name="settings_running_sensor">Рухомий</string>
|
||||
<string name="settings_running_sensor">Біг</string>
|
||||
<string name="settings_generic_sensor">Загальний</string>
|
||||
<string name="settings_layout_reset_confirm_title">Скинути ваші макети\?</string>
|
||||
<string name="settings_import_section">Імпорт</string>
|
||||
<string name="stats_sensors_power">Сила</string>
|
||||
<string name="stats_sensors_cadence">Каденція</string>
|
||||
<string name="stats_sensors_power">Потужність</string>
|
||||
<string name="stats_sensors_cadence">Каденс</string>
|
||||
<string name="stats_sensors_heart_rate">Частота серцебиття</string>
|
||||
<string name="settings_defaults_summary">Переважні одиниці виміру, активність за замовчуванням, Ім\'я треку за замовчуванням</string>
|
||||
<string name="settings_defaults_summary">Переважні одиниці виміру, Активність за замовчуванням, Назва треку за замовчуванням</string>
|
||||
<string name="settings_defaults_title">Одиниці виміру і дії за замовчуванням</string>
|
||||
<string name="settings_ui_title">Інтерфейс користувача</string>
|
||||
<string name="settings_ui_summary">Зовнішній вигляд, Макет запису, Тема</string>
|
||||
<string name="settings_sensors_title">Датчики</string>
|
||||
<string name="settings_announcements_statistics_title">Голосові оголошення</string>
|
||||
<string name="settings_gps_summary">Інтервал часу, інтервал відстані, Максимальна відстань, точність</string>
|
||||
<string name="settings_sensors_summary">Датчики Їзди На Велосипеді, Окружність Колеса, Датчики Ходу</string>
|
||||
<string name="settings_gps_summary">Інтервал часу, Інтервал відстані, Макс. відстань, Точність</string>
|
||||
<string name="settings_sensors_summary">Датчики велосипеда, Окружність колеса, Датчики ходу</string>
|
||||
<string name="settings_cycling_sensor">Їзда на велосипеді</string>
|
||||
<string name="settings_layout_reset">Скидання Користувальницького Макета</string>
|
||||
<string name="generic_undo">Відмінивши</string>
|
||||
<string name="settings_layout_reset">Скинути користувацький макет</string>
|
||||
<string name="generic_undo">Скасувати</string>
|
||||
<string name="aggregated_stats_filter_no_results">Для зазначеного фільтра немає дій</string>
|
||||
<string name="stats_coordinates">Широта/Довгота</string>
|
||||
<string name="settings_sensor_bluetooth_service_filter_on">Показувати лише пристрої Bluetooth, які оголошують про необхідні послуги</string>
|
||||
<string name="settings_layout_reset_done">Всі налаштування макета були повернуті до значень за замовчуванням</string>
|
||||
<string name="settings_layout_reset_confirm_message">Всі налаштування макета будуть повернуті до значень за замовчуванням. Це не призведе до видалення будь-яких треків на пристрої.</string>
|
||||
<string name="settings_layout_reset_confirm_message">Усі налаштування макета буде повернуто до значень за замовчуванням. Це не призведе до видалення жодних треків з пристрою.</string>
|
||||
<string name="custom_layout_list_title">Макет</string>
|
||||
<string name="generic_today">Сьогодні</string>
|
||||
<string name="generic_yesterday">Вчора</string>
|
||||
<string name="export_cannot_write_to_dir">Не вдається виконати запис до каталогу експорту.</string>
|
||||
<string name="export_dir_not_writable">" (недоступно для запису!)"</string>
|
||||
<string name="location_coordinate">%1$s°</string>
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f миль / год (рекомендовано)</string>
|
||||
<string name="value_float_mile_hour_recommended">%1$.1f миль/год (рекомендовано)</string>
|
||||
<string name="location_latitude_longitude">%1$s, %2$s</string>
|
||||
<string name="value_float_kilometer_hour_recommended">%1$.1f км / год (рекомендовано)</string>
|
||||
<string name="value_float_mile_hour">%1$.1f km/h</string>
|
||||
<string name="value_float_kilometer_hour_recommended">%1$.1f км/год (рекомендовано)</string>
|
||||
<string name="value_float_mile_hour">%1$.1f миль/год</string>
|
||||
<string name="description_speed_source_gps">GPS</string>
|
||||
<string name="value_float_kilometer_hour">%1$.1f km/h</string>
|
||||
<string name="value_float_kilometer_hour">%1$.1f км/год</string>
|
||||
<string name="settings_gps_sensor">GPS</string>
|
||||
<string name="no_compatible_file_manager_installed">Не встановлено сумісний файловий менеджер</string>
|
||||
<string name="help_data_google_play_services_content">Ні</string>
|
||||
@@ -508,12 +508,12 @@ limitations under the License.
|
||||
<string name="activity_type_swimming">плавання</string>
|
||||
<string name="generic_dismiss">Відхилити</string>
|
||||
<string name="settings_export_filename_title">Формат імені файлу</string>
|
||||
<string name="description_pace_nautical">Темп (хв/мм)</string>
|
||||
<string name="description_pace_nautical">Темп (хв/мор. милю)</string>
|
||||
<string name="gps_recording_status">%1$s: %2$s</string>
|
||||
<string name="description_speed_nautical">Швидкість (вузли)</string>
|
||||
<string name="image_discard">Скасувати</string>
|
||||
<string name="sensor_state_cadence_unit">об/хв</string>
|
||||
<string name="sensor_state_heart_rate_unit">уд/хв</string>
|
||||
<string name="image_discard">Відкинути</string>
|
||||
<string name="sensor_state_cadence_unit">об./хв</string>
|
||||
<string name="sensor_state_heart_rate_unit">уд./хв</string>
|
||||
<string name="sensor_state_power_unit">Вт</string>
|
||||
<string name="settings_recording_min_required_accuracy_summary">Місцезнаходження ігнорується, якщо його точність перевищує %1$s</string>
|
||||
<string name="settings_announcements_average_heart_rate">Середній пульс</string>
|
||||
@@ -529,17 +529,17 @@ limitations under the License.
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">Записані дані не будуть автоматично передаватись іншим програмам.</string>
|
||||
<string name="settings_public_api_package_title">Назва пакета для публічного API</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_on">Програма, яка почала запис, також може отримати доступ до записаних даних.</string>
|
||||
<string name="unit_nautical_mile">Морські милі</string>
|
||||
<string name="unit_nautical_mile">морські милі</string>
|
||||
<string name="settings_public_api_enabled_summary_on">Інші встановлені програми можуть запускати або зупиняти записи.</string>
|
||||
<string name="settings_public_api_enabled_summary_off">Тільки OpenTracks може запускати та зупиняти записи.</string>
|
||||
<string name="track_delete_number_of_tracks">%1$d треків</string>
|
||||
<string name="settings_public_api_dashboard_enabled_title">Автоматична передача даних</string>
|
||||
<string name="settings_public_api_enabled_title">Публічний API</string>
|
||||
<string name="settings_public_api_disabled_toast">Публічний API OpenTracks вимкнено: його можна ввімкнути в налаштуваннях.</string>
|
||||
<string name="track_discarding">Скасування Доріжки</string>
|
||||
<string name="track_discarding">Скасування треку</string>
|
||||
<string name="unit_knots">вузли</string>
|
||||
<string name="settings_sensor_heart_rate_max">Зони: максимальна частота серцевих скорочень (уд/хв)</string>
|
||||
<string name="settings_stats_units_nautical">Морська (морські милі, фут)</string>
|
||||
<string name="settings_sensor_heart_rate_max">Зони: максимальна частота серцевих скорочень (уд./хв)</string>
|
||||
<string name="settings_stats_units_nautical">Морська (морські милі, фути)</string>
|
||||
<string name="settings_api_summary">API дистанційного керування та статистики для інших програм</string>
|
||||
<string name="voice_per_kilometer">за кілометр</string>
|
||||
<string name="pace">Темп</string>
|
||||
@@ -549,7 +549,7 @@ limitations under the License.
|
||||
<string name="voice_per_mile">за милю</string>
|
||||
<string name="voice_on_device_speaker_title">Використовувати динамік пристрою</string>
|
||||
<string name="lap_time">Час за коло</string>
|
||||
<string name="unit_minute_per_nautical_mile">хв/морська миля</string>
|
||||
<string name="unit_minute_per_nautical_mile">хв/морську милю</string>
|
||||
<string name="no_compatible_camera_installed">Не встановлено сумісну програму камери</string>
|
||||
<string name="no_compatible_gallery_installed">Не встановлено сумісну програму галереї зображень</string>
|
||||
<string name="report_bug">Повідомити про помилку</string>
|
||||
@@ -564,5 +564,22 @@ limitations under the License.
|
||||
<string name="show_on_dashboard_not_installed">Не встановлено програму, що підтримує OpenTracks Dashboard API.</string>
|
||||
<string name="value_float_knots_recommended">%1$.1f вузлів (рекомендовано)</string>
|
||||
<string name="sensor_state_power_max">Макс. потужність</string>
|
||||
<string name="power">Сила</string>
|
||||
</resources>
|
||||
<string name="power">Потужність</string>
|
||||
<string name="settings_locale_title">Мова</string>
|
||||
<string name="sensor_state_pressure">Барометр</string>
|
||||
<string name="value_int_seconds">%1$d с (рекомендовано)</string>
|
||||
<string name="marker_detail_title">%1$d з %2$d: %3$s</string>
|
||||
<string name="voiceHoursPlural">{n, plural, =1 {1 година} few {{n,number} години} other {{n,number} годин} }</string>
|
||||
<string name="voiceMinutesPlural">{n, plural, =1 {1 хвилина} few {{n,number} хвилини} other {{n,number} хвилин} }</string>
|
||||
<string name="voiceSecondsPlural">{n, plural, =1 {1 секунда} few {{n,number} секунди} other {{n,number} секунд} }</string>
|
||||
<string name="voiceSpeedKilometersPerHourPlural">{n, plural, =1 {1 кілометр за годину} other {{n,number,#.0} кілометра за годину} }</string>
|
||||
<string name="voiceSpeedMilesPerHourPlural">{n, plural, =1 {1 миля за годину} other {{n,number,#.0} милі за годину} }</string>
|
||||
<string name="voiceSpeedMKnotsPlural">{n, plural, =1 {1 вузол} other {{n,number,#.0} вузла} }</string>
|
||||
<string name="voiceSpeed">{n, plural, =1 {1} other {{n,number,#.0}} }</string>
|
||||
<string name="voiceDistanceKilometersPlural">{n, plural, =1 {1 кілометр} other {{n,number,#.0} кілометра} }</string>
|
||||
<string name="voiceDistanceMilesPlural">{n, plural, =1 {1 миля} other {{n,number,#.0} милі} }</string>
|
||||
<string name="voiceDistanceNauticalMilesPlural">{n, plural, =1 {1 морська миля} other {{n,number,#.0} морської милі} }</string>
|
||||
<string name="voiceDistance">{n, plural, =1 {1 } other {{n,number,#.0}} }</string>
|
||||
<string name="power_x_watt">Потужність {x, plural, =1 {1 ват} few {# вати} other {# ват} }</string>
|
||||
<string name="error_crash_title">%1$s аварійно завершив роботу</string>
|
||||
</resources>
|
||||
|
||||
@@ -629,7 +629,7 @@ limitations under the License.
|
||||
<string name="power">功率</string>
|
||||
<string name="power_x_watt">功率 {x, plural, =1 {1 瓦特} other {# 瓦特} }</string>
|
||||
<string name="create_marker_error">无法创建标记,请重试。</string>
|
||||
<string name="stats_gain_loss">增益与损失</string>
|
||||
<string name="stats_gain_loss">上升与下降</string>
|
||||
<string name="marker_detail_title">%1$d / %2$d:%3$s</string>
|
||||
<string name="settings_night_mode_option_system_dynamic">跟随系统(动态配色)</string>
|
||||
<string name="settings_night_mode_option_day_dynamic">白天模式(动态配色)</string>
|
||||
|
||||
@@ -44,7 +44,7 @@ limitations under the License.
|
||||
<string name="sensor_unit_power" translatable="false">w</string>
|
||||
|
||||
<string name="value_unknown" translatable="false">-</string>
|
||||
<string name="empty" translatable="false"></string>
|
||||
<string name="empty" translatable="false" />
|
||||
<string name="report_issue_link" translatable="false">https://codeberg.org/OpenTracksApp/OpenTracks/issues/new?labels=bug&template=bug_report.md&body=%1$s</string>
|
||||
|
||||
<string-array name="marker_types">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user