Import/export track UUID (necessary for synchronization).

If a track with a UUID is presented in OpenTracks, it cannot be imported again (enforced by database constraint).
Fixes #307.
This commit is contained in:
Dennis Guse
2020-07-06 20:20:46 +02:00
parent db1167ead5
commit f8afa9dbad
15 changed files with 246 additions and 482 deletions
@@ -205,9 +205,9 @@ public class ImportProgressDialogFragment extends DialogFragment {
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(context);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext(), -1L);
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext());
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(context, -1L, file.getUri());
trackImporter = new KmzTrackImporter(context, file.getUri());
} else {
Log.i(TAG, "Unsupported file format.");
return false;
@@ -33,6 +33,7 @@ import de.dennisguse.opentracks.util.StringUtils;
*
* @author Sandor Dornbush
*/
//TODO Export waypoints
public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -93,10 +94,15 @@ public class GpxTrackWriter implements TrackWriter {
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemes/TrackPointExtension/v2\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
printWriter.println("xsi:schemaLocation=" +
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd"
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd\">");
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
printWriter.println("<metadata>");
Track track = tracks[0];
@@ -127,17 +133,15 @@ public class GpxTrackWriter implements TrackWriter {
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
}
@@ -158,7 +162,11 @@ public class GpxTrackWriter implements TrackWriter {
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
printWriter.println("<extensions>");
printWriter.println("<topografix:color>c0c0c0</topografix:color>");
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
printWriter.println("</extensions>");
}
}
@@ -108,7 +108,10 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\">");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">");
//TODO ADD xsi:schemaLocation here!
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
@@ -204,6 +207,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<icon>" + StringUtils.formatCData(track.getIcon()) + "</icon>");
printWriter.println("<opentracks:trackid>" + track.getUuid() + "</opentracks:trackid>");
}
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
@@ -460,7 +464,7 @@ public class KmlTrackWriter implements TrackWriter {
/**
* Writes a simple array style.
*
* @param name the name of the simple array.
* @param name the name of the simple array.
* @param extendedDataType the extended data display name
*/
private void writeSimpleArrayStyle(String name, String extendedDataType) {
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.util.FileUtils;
* @author Jimmy Shih
*/
public class KmzTrackExporter implements TrackExporter {
private static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml";
@@ -49,7 +50,7 @@ public class KmzTrackExporter implements TrackExporter {
private static final int BUFFER_SIZE = 4096;
private final ContentProviderUtils contentProviderUtils;
private final TrackExporter fileTrackExporter;
private final FileTrackExporter fileTrackExporter;
private final Track[] tracks;
private final boolean exportPhotos;
@@ -58,10 +59,10 @@ public class KmzTrackExporter implements TrackExporter {
* Constructor.
*
* @param contentProviderUtils the content provider utils
* @param trackExporter the file track exporter
* @param trackExporter the file track exporter
* @param tracks the tracks to export
*/
public KmzTrackExporter(ContentProviderUtils contentProviderUtils, TrackExporter trackExporter, Track[] tracks, boolean exportPhotos) {
public KmzTrackExporter(ContentProviderUtils contentProviderUtils, FileTrackExporter trackExporter, Track[] tracks, boolean exportPhotos) {
this.contentProviderUtils = contentProviderUtils;
this.fileTrackExporter = trackExporter;
this.tracks = tracks;
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.net.Uri;
import android.util.Log;
@@ -62,11 +63,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The maximum number of buffered locations for bulk-insertion
private static final int MAX_BUFFERED_LOCATIONS = 512;
private final Context context;
private final long importTrackId;
private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval;
private long importTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private final List<Long> trackIds = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
@@ -96,19 +98,17 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The SAX locator to get the current line information
private Locator locator;
/**
* @param context the context
* @param importTrackId the track id to import to. -1L to import to a new track.
*/
@Deprecated
// Remove importTrackId
AbstractFileTrackImporter(Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
this.context = context;
this.importTrackId = importTrackId;
this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
}
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils, long importTrackId) {
this(context, contentProviderUtils);
this.importTrackId = importTrackId;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
@@ -134,12 +134,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
saxParser.parse(inputStream, this);
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
if (trackIds.size() != 1) {
Log.d(TAG, trackIds.size() + " tracks imported");
cleanImport();
return -1L;
throw new MultiTracksImportException();
}
return trackIds.get(0);
} catch (IOException | SAXException | ParserConfigurationException e) {
} catch (MultiTracksImportException | IOException | SAXException | SQLiteConstraintException | ParserConfigurationException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
@@ -544,4 +542,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The number of buffered locations
int numBufferedTrackPoints = 0;
}
@Deprecated
//TODO According to the reader AND writer, we support multiple tracks per file; we should implement it here also.
private static class MultiTracksImportException extends RuntimeException {
}
}
@@ -45,6 +45,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
@@ -64,7 +65,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
@VisibleForTesting
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, -1L, contentProviderUtils);
super(context, contentProviderUtils);
}
@Override
@@ -144,6 +145,10 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
cadence = content.trim();
}
break;
case TAG_ID:
if (content != null) {
uuid = content.trim();
}
}
// Reset element content
@@ -57,6 +57,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_STYLE_URL = "styleUrl";
private static final String TAG_VALUE = "value";
private static final String TAG_WHEN = "when";
private static final String TAG_UUID = "opentracks:trackid";
private static final String ATTRIBUTE_NAME = "name";
@@ -68,23 +69,17 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private ArrayList<Float> heartRateList;
private ArrayList<Float> powerList;
/**
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
*/
@Deprecated //TODO Do not pass importTrackId here; get it from somewhere else
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, new ContentProviderUtils(context));
public KmlFileTrackImporter(Context context) {
this(context, new ContentProviderUtils(context));
}
@VisibleForTesting
KmlFileTrackImporter(Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
super(context, importTrackId, contentProviderUtils);
KmlFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, contentProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY:
@@ -109,50 +104,70 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
//TODO Check if order is relevant (uses localname and tag); and convert to switch statement
if (tag.equals(TAG_KML)) {
onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
// Note that a track is contained in a Placemark, calling onWaypointend is save since waypointType is not set for a track.
onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_GX_TRACK)) {
onTrackSegmentEnd();
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onExtendedDataValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_ICON)) {
if (content != null) {
icon = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
}
} else if (localName.equals(TAG_WHEN)) {
if (content != null) {
time = content.trim();
}
} else if (localName.equals(TAG_STYLE_URL)) {
if (content != null) {
waypointType = content.trim();
}
} else if (localName.equals(TAG_HREF)) {
if (content != null) {
photoUrl = content.trim();
}
switch (tag) {
case TAG_KML:
onFileEnd();
break;
case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY:
// Note that a track is contained in a Placemark, calling onWaypointend is save since waypointType is not set for a track.
onWaypointEnd();
break;
case TAG_COORDINATES:
onWaypointLocationEnd();
break;
case TAG_GX_MULTI_TRACK:
onTrackEnd();
break;
case TAG_GX_TRACK:
onTrackSegmentEnd();
break;
case TAG_GX_COORD:
onTrackPointEnd();
break;
case TAG_GX_VALUE:
onExtendedDataValueEnd();
break;
case TAG_NAME:
if (content != null) {
name = content.trim();
}
break;
case TAG_UUID:
if (content != null) {
uuid = content.trim();
}
break;
case TAG_DESCRIPTION:
if (content != null) {
description = content.trim();
}
break;
case TAG_ICON:
if (content != null) {
icon = content.trim();
}
break;
case TAG_VALUE:
if (content != null) {
category = content.trim();
}
break;
case TAG_WHEN:
if (content != null) {
time = content.trim();
}
break;
case TAG_STYLE_URL:
if (content != null) {
waypointType = content.trim();
}
break;
case TAG_HREF:
if (content != null) {
photoUrl = content.trim();
}
break;
}
// Reset element content
@@ -52,18 +52,15 @@ public class KmzTrackImporter implements TrackImporter {
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final long importTrackId;
private long importTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private final Uri uriKmzFile;
/**
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
* @param uriFile URI of the kmz file.
* @param context the context
* @param uriFile URI of the kmz file.
*/
@Deprecated //TODO Do not pass importTrackId here; get it from somewhere else
public KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
public KmzTrackImporter(Context context, Uri uriFile) {
this.context = context;
this.importTrackId = importTrackId;
this.uriKmzFile = uriFile;
}
@@ -244,7 +241,7 @@ public class KmzTrackImporter implements TrackImporter {
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
return kmlFileTrackImporter.importFile(byteArrayInputStream);