forked from upstream-mirrors/OpenTracks
@@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager;
|
|||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
import androidx.test.rule.ServiceTestRule;
|
import androidx.test.rule.ServiceTestRule;
|
||||||
|
|
||||||
@@ -27,15 +28,19 @@ import org.junit.Test;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
@@ -426,6 +431,36 @@ public class ExportImportTest {
|
|||||||
assertNull(trackImported);
|
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() {
|
private void assertMarkers() {
|
||||||
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
|
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
|
||||||
|
|
||||||
|
|||||||
@@ -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",,,,,,,,,,,
|
||||||
|
@@ -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_AND_SENSORDATA = 6;
|
||||||
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
|
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
|
||||||
|
private static final int URI_CSV = 8;
|
||||||
private static final int URI_SHARE_PICTURE = 8;
|
|
||||||
|
|
||||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
private static final String TRACKID_DELIMITER = "_";
|
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.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.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:
|
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
|
||||||
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
|
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
|
||||||
|
|
||||||
|
case URI_CSV:
|
||||||
|
return TrackFileFormat.CSV;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
|
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
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.GPXTrackExporter;
|
||||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||||
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
||||||
@@ -100,6 +101,23 @@ public enum TrackFileFormat {
|
|||||||
public String getExtension() {
|
public String getExtension() {
|
||||||
return "gpx";
|
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";
|
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
|
||||||
|
|||||||
@@ -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: ,
|
||||||
|
* <p>
|
||||||
|
* 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<Column> columns = Collections.unmodifiableList(Arrays.asList(
|
||||||
|
new Column("time", null),
|
||||||
|
new Column("trackpoint_type", t -> quote(t.getType().name())),
|
||||||
|
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||||
|
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||||
|
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""),
|
||||||
|
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
|
||||||
|
|
||||||
|
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
|
||||||
|
new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
|
||||||
|
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""),
|
||||||
|
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
|
||||||
|
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
|
||||||
|
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
|
||||||
|
new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
prepare(outputStream);
|
||||||
|
|
||||||
|
boolean headerWritten = false;
|
||||||
|
|
||||||
|
for (Track track : tracks) {
|
||||||
|
columns.get(0).extractor = t -> quote(StringUtils.formatDateTimeIso8601(t.getTime(), track.getZoneOffset()));
|
||||||
|
|
||||||
|
if (!headerWritten) {
|
||||||
|
writeHeader(columns);
|
||||||
|
headerWritten = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeTrackPoints(columns, track);
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.e(TAG, "Thread interrupted", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeTrackPoints(List<Column> columns, Track track) throws InterruptedException {
|
||||||
|
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||||
|
while (trackPointIterator.hasNext()) {
|
||||||
|
if (Thread.interrupted()) throw new InterruptedException();
|
||||||
|
|
||||||
|
TrackPoint trackPoint = trackPointIterator.next();
|
||||||
|
|
||||||
|
switch (trackPoint.getType()) {
|
||||||
|
case SEGMENT_START_MANUAL:
|
||||||
|
case SEGMENT_END_MANUAL:
|
||||||
|
case SEGMENT_START_AUTOMATIC:
|
||||||
|
case SENSORPOINT:
|
||||||
|
case TRACKPOINT:
|
||||||
|
writeTrackPoint(columns, trackPoint);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepare(OutputStream outputStream) {
|
||||||
|
this.printWriter = new PrintWriter(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
if (printWriter != null) {
|
||||||
|
printWriter.flush();
|
||||||
|
printWriter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeHeader(List<Column> columns) {
|
||||||
|
if (printWriter != null) {
|
||||||
|
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) {
|
||||||
|
if (printWriter != null) {
|
||||||
|
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint))
|
||||||
|
.reduce((s, s2) -> s + "," + s2)
|
||||||
|
.orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||||
|
printWriter.println(columnNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Column {
|
||||||
|
final String columnName;
|
||||||
|
Function<TrackPoint, String> extractor;
|
||||||
|
|
||||||
|
Column(String columnName, Function<TrackPoint, String> extractor) {
|
||||||
|
this.columnName = columnName;
|
||||||
|
this.extractor = extractor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String quote(String content) {
|
||||||
|
return '"' + content + '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,8 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
|||||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
||||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
|
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||||
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||||
TrackFileFormat.GPX
|
TrackFileFormat.GPX,
|
||||||
|
TrackFileFormat.CSV
|
||||||
};
|
};
|
||||||
String[] entries = new String[trackFileFormats.length];
|
String[] entries = new String[trackFileFormats.length];
|
||||||
String[] entryValues = new String[trackFileFormats.length];
|
String[] entryValues = new String[trackFileFormats.length];
|
||||||
|
|||||||
Reference in New Issue
Block a user