diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index 32f4d0071..3c369c958 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.ServiceTestRule; @@ -27,15 +28,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.data.TestDataUtil; @@ -426,6 +431,36 @@ public class ExportImportTest { assertNull(trackImported); } + @LargeTest + @Test + public void csv_export_only() throws TimeoutException, IOException { + setUp(); + + // given + Track track = contentProviderUtils.getTrack(trackId); + + TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context); + + // when + // 1. export + trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + contentProviderUtils.deleteTrack(context, trackId); + + // then + InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export); + String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + + InputStream actual = context.getContentResolver().openInputStream(tmpFileUri); + String actualText = new BufferedReader(new InputStreamReader(actual, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + assertEquals(expectedText, actualText); + } + private void assertMarkers() { assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size()); diff --git a/src/androidTest/res/raw/csv_export.csv b/src/androidTest/res/raw/csv_export.csv new file mode 100644 index 000000000..4d99dd5d6 --- /dev/null +++ b/src/androidTest/res/raw/csv_export.csv @@ -0,0 +1,11 @@ +#time,trackpoint_type,latitude,longitude,altitude,accuracy_horizontal,speed,altitude_gain,altitude_loss,sensor_distance,heartrate,cadence,power +"2020-02-02T03:02:02+01:00","SEGMENT_START_MANUAL",,,,,,,,,,, +"2020-02-02T03:02:03+01:00","TRACKPOINT",3,3,10,10,54,1,1,,,, +"2020-02-02T03:02:04+01:00","SENSORPOINT",,,,,54,1,1,10,66,3,50 +"2020-02-02T03:02:05+01:00","TRACKPOINT",3,3,10,10,18,0,0,2,66,3,50 +"2020-02-02T03:02:06+01:00","SEGMENT_END_MANUAL",,,,,,,,,,, +"2020-02-02T03:02:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,, +"2020-02-02T03:02:21+01:00","TRACKPOINT",3,3,10,10,54,0,0,,,, +"2020-02-02T03:02:22+01:00","SEGMENT_START_AUTOMATIC",3,3,10,10,54,0,0,,,, +"2020-02-02T03:02:23+01:00","TRACKPOINT",3,3,10,10,54,0,0,,,, +"2020-02-02T03:02:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,, \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java b/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java index fd59b5707..e99f4aa39 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java +++ b/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java @@ -53,8 +53,7 @@ public class ShareContentProvider extends CustomContentProvider { private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6; private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7; - - private static final int URI_SHARE_PICTURE = 8; + private static final int URI_CSV = 8; private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); private static final String TRACKID_DELIMITER = "_"; @@ -66,6 +65,7 @@ public class ShareContentProvider extends CustomContentProvider { uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES); + uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getName() + "/*/*", URI_CSV); } /** @@ -146,6 +146,9 @@ public class ShareContentProvider extends CustomContentProvider { case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES: return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES; + case URI_CSV: + return TrackFileFormat.CSV; + default: throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/TrackFileFormat.java b/src/main/java/de/dennisguse/opentracks/io/file/TrackFileFormat.java index 8a52cae79..607cfc984 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/TrackFileFormat.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/TrackFileFormat.java @@ -6,6 +6,7 @@ import java.util.Locale; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.io.file.exporter.CSVTrackExporter; import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter; import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter; import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter; @@ -100,6 +101,23 @@ public enum TrackFileFormat { public String getExtension() { return "gpx"; } + }, + + CSV { + @Override + public TrackExporter createTrackExporter(Context context) { + return new CSVTrackExporter(new ContentProviderUtils(context)); + } + + @Override + public String getMimeType() { + return "text/csv"; + } + + @Override + public String getExtension() { + return "csv"; + } }; private static final String MIME_KMZ = "application/vnd.google-earth.kmz"; diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java new file mode 100644 index 000000000..c3e2c6346 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java @@ -0,0 +1,202 @@ +/* + * Copyright 2011 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.io.file.exporter; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; + +import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.TrackPointIterator; +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.util.StringUtils; + +/** + * Exports the {@link TrackPoint} into a CSV. + * * decimal separator: . + * * column separator: , + *
+ * NOTE:
+ * * {@link Track} data is not exported.
+ * * {@link Marker} data is not exported.
+ */
+public class CSVTrackExporter implements TrackExporter {
+
+ private static final String TAG = CSVTrackExporter.class.getSimpleName();
+
+ private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
+ private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
+
+ static {
+ ALTITUDE_FORMAT.setMaximumFractionDigits(1);
+ ALTITUDE_FORMAT.setGroupingUsed(false);
+
+ COORDINATE_FORMAT.setMaximumFractionDigits(6);
+ COORDINATE_FORMAT.setMaximumIntegerDigits(3);
+ COORDINATE_FORMAT.setGroupingUsed(false);
+
+ SPEED_FORMAT.setMaximumFractionDigits(2);
+ SPEED_FORMAT.setGroupingUsed(false);
+
+ HEARTRATE_FORMAT.setMaximumFractionDigits(0);
+ HEARTRATE_FORMAT.setGroupingUsed(false);
+
+ CADENCE_FORMAT.setMaximumFractionDigits(0);
+ CADENCE_FORMAT.setGroupingUsed(false);
+
+ POWER_FORMAT.setMaximumFractionDigits(0);
+ POWER_FORMAT.setGroupingUsed(false);
+ }
+
+ private final ContentProviderUtils contentProviderUtils;
+
+ private PrintWriter printWriter;
+
+ public CSVTrackExporter(ContentProviderUtils contentProviderUtils) {
+ this.contentProviderUtils = contentProviderUtils;
+ }
+
+ @Override
+ public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
+ return writeTrack(new Track[]{track}, outputStream);
+ }
+
+ @Override
+ public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
+ List