diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java index 2daf4007d..232b7f620 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java @@ -88,6 +88,40 @@ public class DescriptionGeneratorTest { Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true)); } + @Test + public void testGenerateTrackDescriptionWithoutMaxMinElevation() { + Track track = new Track(); + TrackStatistics stats = new TrackStatistics(); + stats.setTotalDistance(20000); + stats.setTotalTime(600000); + stats.setMovingTime(300000); + stats.setMaxSpeed(100); + stats.setMaxElevation(Double.POSITIVE_INFINITY); + stats.setMinElevation(Double.NEGATIVE_INFINITY); + stats.setTotalElevationGain(6000); + stats.setStartTime_ms(START_TIME); + track.setTrackStatistics(stats); + track.setCategory("hiking"); + String expected = //"Created by" + "OpenTracks

" + + "Name: -
" + + "Activity type: hiking
" + + "Description: -
" + + "Total distance: 20.00 km (12.4 mi)
" + + "Total time: 10:00
" + + "Moving time: 05:00
" + + "Average speed: 120.00 km/h (74.6 mi/h)
" + + "Average moving speed: 240.00 km/h (149.1 mi/h)
" + + "Max speed: 360.00 km/h (223.7 mi/h)
" + + "Average pace: 0:30 min/km (0:48 min/mi)
" + + "Average moving pace: 0:15 min/km (0:24 min/mi)
" + + "Fastest pace: 0:10 min/km (0:16 min/mi)
" + + "Elevation gain: 6000 m (19685 ft)
" + + "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "
"; + + Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true)); + } + /** * Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}. diff --git a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java index e4933e6f2..be850d530 100644 --- a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java +++ b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java @@ -126,10 +126,14 @@ public class DescriptionGenerator { writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak); // Max elevation - writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak); + if (stats.hasElevationMax()) { + writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak); + } // Min elevation - writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak); + if (stats.hasElevationMin()) { + writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak); + } // Elevation gain writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 393ab19ca..4ed5314f7 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java @@ -32,6 +32,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.StringUtils; /** @@ -162,9 +163,10 @@ public class KmlTrackWriter implements TrackWriter { @Override public void writeWaypoint(Waypoint waypoint) { if (printWriter != null && exportTrackDetail) { - if (waypoint.hasPhoto() && exportPhotos) { + boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, waypoint.getTrackId(), waypoint.getPhotoURI()) != null; + if (waypoint.hasPhoto() && exportPhotos && existsPhoto) { float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation()); - writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), waypoint.getLocation(), waypoint.getPhotoUrl(), heading); + writePhotoOverlay(waypoint, heading); } else { writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation()); } @@ -322,32 +324,29 @@ public class KmlTrackWriter implements TrackWriter { } /** - * Writes a photo overlay. - * @param name the name - * @param category the category - * @param description the description - * @param location the location - * @param photoUrl the photo url - * @param heading the heading + * Writes a photo overlay from waypoint. + * + * @param waypoint Waypoint object. + * @param heading the heading. */ - private void writePhotoOverlay(String name, String category, String description, Location location, String photoUrl, float heading) { - if (location != null && exportTrackDetail) { + private void writePhotoOverlay(Waypoint waypoint, float heading) { + if (exportTrackDetail) { printWriter.println(""); - printWriter.println("" + StringUtils.formatCData(name) + ""); - printWriter.println("" + StringUtils.formatCData(description) + ""); + printWriter.println("" + StringUtils.formatCData(waypoint.getName()) + ""); + printWriter.println("" + StringUtils.formatCData(waypoint.getDescription()) + ""); printWriter.print(""); - printWriter.print("" + location.getLongitude() + ""); - printWriter.print("" + location.getLatitude() + ""); + printWriter.print("" + waypoint.getLocation().getLongitude() + ""); + printWriter.print("" + waypoint.getLocation().getLatitude() + ""); printWriter.print("20"); printWriter.print("" + heading + ""); printWriter.print("90"); printWriter.println(""); - printWriter.println("" + getTime(location) + ""); + printWriter.println("" + getTime(waypoint.getLocation()) + ""); printWriter.println("#" + KmlTrackWriter.WAYPOINT_STYLE + ""); - writeCategory(category); + writeCategory(waypoint.getCategory()); if (exportPhotos) { - printWriter.println("" + KmzTrackExporter.buildKmzImageFilePath(Uri.parse(photoUrl)) + ""); + printWriter.println("" + KmzTrackExporter.buildKmzImageFilePath(waypoint) + ""); } printWriter.print(""); @@ -358,7 +357,7 @@ public class KmlTrackWriter implements TrackWriter { printWriter.print("45"); printWriter.println(""); printWriter.println(""); - printWriter.println("" + getCoordinates(location, ",") + ""); + printWriter.println("" + getCoordinates(waypoint.getLocation(), ",") + ""); printWriter.println(""); printWriter.println(""); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java index ef24556dc..ca4fc59a2 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java @@ -112,7 +112,11 @@ public class KmzTrackExporter implements TrackExporter { } Waypoint waypoint = contentProviderUtils.createWaypoint(cursor); if (waypoint.hasPhoto()) { - addImage(context, zipOutputStream, waypoint.getPhotoUrl()); + Uri uriPhoto = waypoint.getPhotoURI(); + boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null; + if (existsPhoto) { + addImage(context, zipOutputStream, uriPhoto, waypoint); + } } cursor.moveToNext(); @@ -122,11 +126,9 @@ public class KmzTrackExporter implements TrackExporter { } } - private void addImage(Context context, ZipOutputStream zipOutputStream, String photoUrl) throws IOException { - Uri uri = Uri.parse(photoUrl); - + private void addImage(Context context, ZipOutputStream zipOutputStream, Uri uri, Waypoint waypoint) throws IOException { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { - ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(uri)); + ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(waypoint)); zipOutputStream.putNextEntry(zipEntry); if (inputStream == null) throw new FileNotFoundException(); @@ -149,11 +151,13 @@ public class KmzTrackExporter implements TrackExporter { } /** - * Builds and returns the path for the file that will be saved inside KMZ_IMAGES_DIR. + * Builds and returns the path for the image that will be saved inside KMZ_IMAGES_DIR for the waypoint. * - * @param uri URI object. + * @param waypoint Waypoint object. */ - public static String buildKmzImageFilePath(Uri uri) { - return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment()); + public static String buildKmzImageFilePath(Waypoint waypoint) { + String ext = FileUtils.getExtension(waypoint.getPhotoUrl()); + ext = ext == null ? "" : "." + ext; + return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(waypoint.getId() + ext); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java index 9c056342e..00dd2393b 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java @@ -313,6 +313,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track */ protected TrackPoint getTrackPoint() throws SAXException { TrackPoint trackPoint = createTrackPoint(); + if (trackPoint == null) { + throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint)); + } // Calculate derived attributes from the previous point if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) { @@ -380,23 +383,15 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track return null; } - Uri externalPhotoUri = Uri.parse(externalPhotoUrl); - if (externalPhotoUri == null) { - Log.w(TAG, "Could not parse external photo url."); + String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl); + File file = FileUtils.getPhotoFileIfExists(context, importTrackId, Uri.parse(importFileName)); + if (file != null) { + Uri photoUri = FileUtils.getUriForFile(context, file); + return "" + photoUri; + } + else { return null; } - String filename = externalPhotoUri.getLastPathSegment(); - if (filename == null) { - Log.w(TAG, "External photo contains no filename."); - return null; - } - - File dir = FileUtils.getPhotoDir(context, importTrackId); - File file = new File(dir, filename); - - Uri photoUri = FileUtils.getUriForFile(context, file); - - return "" + photoUri; } /** diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java index 5c24c0ad2..96a701d9b 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java @@ -99,7 +99,7 @@ class ImportAsyncTask extends AsyncTask { for (DocumentFile candidate : file.listFiles()) { if (!candidate.isDirectory()) { String extension = FileUtils.getExtension(candidate.getName()); - if (trackFileFormat.getExtension().equals(extension)) { + if (extension != null && trackFileFormat.getExtension().equals(extension)) { files.add(candidate); } } else { @@ -186,7 +186,7 @@ class ImportAsyncTask extends AsyncTask { Uri uri = contentProviderUtils.insertTrack(new Track()); long newId = Long.parseLong(uri.getLastPathSegment()); - trackImporter = new KmzTrackImporter(importActivity, newId); + trackImporter = new KmzTrackImporter(importActivity, newId, file.getUri()); } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java index 3db706bdd..beda05523 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java @@ -17,6 +17,7 @@ package de.dennisguse.opentracks.io.file.importer; import android.content.Context; +import android.net.Uri; import android.util.Log; import java.io.ByteArrayInputStream; @@ -25,9 +26,13 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter; import de.dennisguse.opentracks.util.FileUtils; @@ -42,58 +47,190 @@ public class KmzTrackImporter implements TrackImporter { private static final String TAG = KmzTrackImporter.class.getSimpleName(); + public static final List KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png"); + private static final int BUFFER_SIZE = 4096; private final Context context; private final long importTrackId; + private Uri uriKmzFile; /** * Constructor. * * @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. */ - KmzTrackImporter(Context context, long importTrackId) { + KmzTrackImporter(Context context, long importTrackId, Uri uriFile) { this.context = context; this.importTrackId = importTrackId; + this.uriKmzFile = uriFile; } @Override public long importFile(InputStream inputStream) { - long trackId = importTrackId; - try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { + long trackId; + + if (!copyKmzImages()) { + cleanImport(context, importTrackId); + return -1L; + } + + trackId = findAndParseKmlFile(inputStream); + if (trackId == -1L) { + cleanImport(context, importTrackId); + return -1L; + } + + deleteOrphanImages(context, trackId); + + return trackId; + } + + /** + * Copies all images that are inside KMZ to OpenTracks external storage. + * + * @return false if there are errors or true otherwise. + */ + private boolean copyKmzImages() { + try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile); + ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (Thread.interrupted()) { Log.d(TAG, "Thread interrupted"); - cleanImport(context, trackId); + return false; + } + + String fileName = zipEntry.getName(); + if (hasImageExtension(fileName)) { + readAndSaveImageFile(zipInputStream, importNameForFilename(fileName)); + } + + zipInputStream.closeEntry(); + } + + return true; + } catch (IOException e) { + Log.e(TAG, "Unable to import file", e); + return false; + } + } + + /** + * From path fileName generates an import unique name and returns it. + * The name generator is simple: change the path fileName with '-' instead of File.separatorChar. + * + * @param fileName the file name. + */ + public static String importNameForFilename(String fileName) { + // TODO this tricky code for maintain backward compatibility must be deleted some day. + /* + * In versions before v3.5.0 photo URL in KML files were wrong. + * For compatibility reasons it checks if fileName begins with "content://" or "file://". + * All fileName begins with "content:/" or "file://" are cooked. + * We cannot guess what's the folder name where images are so we use "images" that was the folder name expected in versions before v3.5.0. + */ + if (fileName.startsWith("content://") || fileName.startsWith("file://")) { + fileName = "images/" + fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1); + } + + return fileName.replace(File.separatorChar, '-'); + } + + /** + * Returns true if fileName ends with some of the KMZ_IMAGES_EXT suffixes. + * Otherwise returns false. + */ + private boolean hasImageExtension(String fileName) { + if (fileName == null) { + return false; + } + + String fileExt = FileUtils.getExtension(fileName.toLowerCase()); + if (fileExt == null) { + return false; + } + + if (KMZ_IMAGES_EXT.contains(fileExt)) { + return true; + } + + return false; + } + + /** + * Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it. + * + * @param inputStream kmz input stream. + * @return -1 if error or the id of the track otherwise. + */ + private long findAndParseKmlFile(InputStream inputStream) { + try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { + ZipEntry zipEntry; + long trackId = -1L; + + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (Thread.interrupted()) { + Log.d(TAG, "Thread interrupted"); return -1L; } + String fileName = zipEntry.getName(); if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) { trackId = parseKml(zipInputStream); if (trackId == -1L) { Log.d(TAG, "Unable to parse kml in kmz"); - cleanImport(context, trackId); return -1L; } - } else { - String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar; - if (fileName.startsWith(prefix)) { - readImageFile(zipInputStream, fileName.substring(prefix.length())); - } } + zipInputStream.closeEntry(); } + return trackId; } catch (IOException e) { Log.e(TAG, "Unable to import file", e); - cleanImport(context, trackId); return -1L; } } + /** + * Deletes all images that remained in external storage that doesn't have waypoint (marker) associated. + * + * @param context the Context object. + * @param trackId the id of the Track. + */ + private void deleteOrphanImages(Context context, long trackId) { + if (trackId != 1L) { + // 1.- Gets all photo names in the waypoints of the track identified by id. + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + List waypoints = contentProviderUtils.getWaypoints(trackId); + List photosName = new ArrayList<>(); + for (Waypoint w : waypoints) { + if (w.hasPhoto()) { + String photoUrl = Uri.decode(w.getPhotoUrl()); + photosName.add(photoUrl.substring(photoUrl.lastIndexOf(File.separatorChar) + 1)); + } + } + + // 2.- Deletes all orphan photos from external storage. + File dir = FileUtils.getPhotoDir(context, trackId); + if (dir.exists() && dir.isDirectory()) { + for (File file : dir.listFiles()) { + if (!photosName.contains(file.getName())) { + file.delete(); + } + } + if (dir.listFiles().length == 0) { + dir.delete(); + } + } + } + } + /** * Cleans up import. * @@ -147,12 +284,12 @@ public class KmzTrackImporter implements TrackImporter { } /** - * Reads an image file. + * Reads an image file (zipInputStream) and save it in a file called fileName inside photo folder. * * @param zipInputStream the zip input stream * @param fileName the file name */ - private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException { + private void readAndSaveImageFile(ZipInputStream zipInputStream, String fileName) throws IOException { if (importTrackId == -1L || fileName.equals("")) { return; } diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index 37cd6b273..fa4fb271d 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -176,6 +176,10 @@ public class TrackStatistics { this.maxSpeed_mps = maxSpeed; } + public boolean hasElevationMin() { + return !Double.isInfinite(getMinElevation()); + } + /** * Gets the minimum elevation. * This is calculated from the smoothed elevation, so this can actually be more than the current elevation. @@ -193,6 +197,10 @@ public class TrackStatistics { elevationExtremities.setMin(elevation); } + public boolean hasElevationMax() { + return !Double.isInfinite(getMaxElevation()); + } + /** * Gets the maximum elevation. * This is calculated from the smoothed elevation, so this can actually be less than the current elevation. diff --git a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java index 71368a196..3a609b247 100644 --- a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java @@ -95,11 +95,15 @@ public class FileUtils { /** * Gets the extension from a file name. - * Returns null if there is no extension. * * @param fileName the file name + * @return null if there is no extension or fileName is null. */ public static String getExtension(String fileName) { + if (fileName == null) { + return null; + } + int index = fileName.lastIndexOf('.'); if (index == -1) { return null; @@ -250,4 +254,34 @@ public class FileUtils { public static Uri getUriForFile(Context context, File file) { return FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file); } + + /** + * Checks that there is a file inside track photo directory whose name is the same that uri file. + * If there is a file inside photo directory whose name is the same that uri then returns File. Otherwise returns null. + * + * @param context the Context. + * @param trackId the id of the Track. + * @param uri the uri to check. + * @return File object or null. + */ + public static File getPhotoFileIfExists(Context context, long trackId, Uri uri) { + if (uri == null) { + Log.w(TAG, "URI object is null."); + return null; + } + + String filename = uri.getLastPathSegment(); + if (filename == null) { + Log.w(TAG, "External photo contains no filename."); + return null; + } + + File dir = FileUtils.getPhotoDir(context, trackId); + File file = new File(dir, filename); + if (!file.exists()) { + return null; + } + + return file; + } }