diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractTrackExporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractTrackExporter.java new file mode 100644 index 000000000..c15da34c1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractTrackExporter.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013 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 com.google.android.apps.mytracks.io.file; + +import android.util.Log; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Abstract class for {@link TrackExporter}. + * + * @author Jimmy Shih + */ +abstract class AbstractTrackExporter implements TrackExporter { + + private static final String TAG = AbstractTrackExporter.class.getSimpleName(); + + private Thread writeThread; + private boolean success = false; + + @Override + public void writeTrack(final OutputStream outputStream) { + writeThread = new Thread() { + @Override + public void run() { + try { + performWrite(outputStream); + success = true; + } catch (InterruptedException e) { + Log.e(TAG, "Unable to perform write", e); + success = false; + } catch (IOException e) { + Log.e(TAG, "Unable to perform write", e); + success = false; + } + } + }; + writeThread.start(); + try { + writeThread.join(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting for write thread to finish", e); + success = false; + } + } + + @Override + public void stopWriteTrack() { + if (writeThread != null && writeThread.isAlive()) { + Log.i(TAG, "Attempting to stop write thread"); + writeThread.interrupt(); + try { + writeThread.join(); + Log.i(TAG, "Write thread stopped"); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting for write thread to stop", e); + success = false; + } + } + } + + @Override + public boolean isSuccess() { + return success; + } + + abstract void performWrite(OutputStream outputStream) throws InterruptedException, IOException; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/FileTrackExporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/FileTrackExporter.java index 6a7616fee..46f0ded8f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/FileTrackExporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/FileTrackExporter.java @@ -28,7 +28,6 @@ import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.database.Cursor; import android.location.Location; -import android.util.Log; import java.io.OutputStream; @@ -38,18 +37,13 @@ import java.io.OutputStream; * @author Sandor Dornbush * @author Rodrigo Damazio */ -public class FileTrackExporter implements TrackExporter { - - private final static String TAG = FileTrackExporter.class.getSimpleName(); +public class FileTrackExporter extends AbstractTrackExporter { private final MyTracksProviderUtils myTracksProviderUtils; private final Track[] tracks; private final TrackWriter trackWriter; private final TrackExporterListener trackExporterListener; - private Thread writeThread; - private boolean success = false; - /** * Constructor. * @@ -59,10 +53,10 @@ public class FileTrackExporter implements TrackExporter { * @param trackFileFormat the track file format * @param trackExporterListener the track export listener */ - public FileTrackExporter(Context context, MyTracksProviderUtils myTracksProviderUtils, - Track[] tracks, TrackFileFormat trackFileFormat, + public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils, + Track[] tracks, TrackFileFormat trackFileFormat, Context context, boolean inZip, TrackExporterListener trackExporterListener) { - this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context), + this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context, inZip), trackExporterListener); } @@ -76,55 +70,17 @@ public class FileTrackExporter implements TrackExporter { } @Override - public boolean isSuccess() { - return success; - } - - @Override - public void writeTrack(final OutputStream outputStream) { - writeThread = new Thread() { - @Override - public void run() { - try { - trackWriter.prepare(outputStream); - trackWriter.writeHeader(tracks[0]); - long startTime = tracks[0].getTripStatistics().getStartTime(); - for (int i = 0; i < tracks.length; i++) { - writeWaypoints(tracks[i]); - long offset = tracks[i].getTripStatistics().getStartTime() - startTime; - writeLocations(tracks[i], offset); - } - trackWriter.writeFooter(); - trackWriter.close(); - success = true; - } catch (InterruptedException e) { - success = false; - } - } - }; - writeThread.start(); - try { - writeThread.join(); - } catch (InterruptedException e) { - Log.e(TAG, "Interrupted while waiting for write to complete", e); - success = false; - } - } - - @Override - public void stopWriteTrack() { - if (writeThread != null && writeThread.isAlive()) { - Log.i(TAG, "Attempting to stop track write"); - writeThread.interrupt(); - - try { - writeThread.join(); - Log.i(TAG, "Track write stopped"); - } catch (InterruptedException e) { - Log.e(TAG, "Interrupted while waiting for writer to stop", e); - success = false; - } + void performWrite(OutputStream outputStream) throws InterruptedException { + trackWriter.prepare(outputStream); + trackWriter.writeHeader(tracks[0]); + long startTime = tracks[0].getTripStatistics().getStartTime(); + for (int i = 0; i < tracks.length; i++) { + writeWaypoints(tracks[i]); + long offset = tracks[i].getTripStatistics().getStartTime() - startTime; + writeLocations(tracks[i], offset); } + trackWriter.writeFooter(); + trackWriter.close(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java index a070b8cc7..f579a2d7c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java @@ -35,6 +35,7 @@ import android.database.Cursor; import android.location.Location; import android.net.Uri; +import java.io.File; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; @@ -68,8 +69,10 @@ public class KmlTrackWriter implements TrackWriter { TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png"; private final Context context; + private final boolean inKmz; private final DescriptionGenerator descriptionGenerator; private final MyTracksProviderUtils myTracksProviderUtils; + private PrintWriter printWriter; private ArrayList powerList = new ArrayList(); private ArrayList cadenceList = new ArrayList(); @@ -78,13 +81,14 @@ public class KmlTrackWriter implements TrackWriter { private boolean hasCadence; private boolean hasHeartRate; - public KmlTrackWriter(Context context) { - this(context, new DescriptionGeneratorImpl(context)); + public KmlTrackWriter(Context context, boolean inKmz) { + this(context, inKmz, new DescriptionGeneratorImpl(context)); } @VisibleForTesting - KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) { + KmlTrackWriter(Context context, boolean inKmz, DescriptionGenerator descriptionGenerator) { this.context = context; + this.inKmz = inKmz; this.descriptionGenerator = descriptionGenerator; this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); } @@ -335,9 +339,8 @@ public class KmlTrackWriter implements TrackWriter { if (location != null) { printWriter.println(""); printWriter.println("" + StringUtils.formatCData(name) + ""); - printWriter.println("" - + StringUtils.formatCData(StringUtils.getCategoryDescription(category, description)) - + ""); + printWriter.println( + "" + StringUtils.formatCData(description) + ""); printWriter.print(""); printWriter.print("" + location.getLongitude() + ""); printWriter.print("" + location.getLatitude() + ""); @@ -345,8 +348,18 @@ public class KmlTrackWriter implements TrackWriter { printWriter.print("" + heading + ""); printWriter.print("90"); printWriter.println(""); + printWriter.println("" + + StringUtils.formatDateTimeIso8601(location.getTime()) + ""); printWriter.println("#" + styleName + ""); - printWriter.println("" + Uri.decode(photoUrl) + ""); + writeCategory(category); + if (inKmz) { + Uri uri = Uri.parse(photoUrl); + printWriter.println("" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar + + uri.getLastPathSegment() + ""); + } else { + printWriter.println("" + Uri.decode(photoUrl) + ""); + } + printWriter.print(""); printWriter.print("10"); printWriter.print("-60"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmzTrackExporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmzTrackExporter.java new file mode 100644 index 000000000..ae11f9b2a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmzTrackExporter.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013 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 com.google.android.apps.mytracks.io.file; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; + +import android.database.Cursor; +import android.net.Uri; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * KMZ track exporter. + * + * @author Jimmy Shih + */ +public class KmzTrackExporter extends AbstractTrackExporter { + + public static final String KMZ_IMAGES_DIR = "images"; + public static final String KMZ_EXTENSION = "kmz"; + public static final int BUFFER_SIZE = 4096; + public static final String KML_FILE_NAME = "doc.kml"; + + private final MyTracksProviderUtils myTracksProviderUtils; + private final TrackExporter trackExporter; + private final Track[] tracks; + + /** + * Constructor. + * + * @param myTracksProviderUtils the my tracks provider utils + * @param trackExporter the track exporter + * @param tracks the tracks to export + */ + public KmzTrackExporter( + MyTracksProviderUtils myTracksProviderUtils, TrackExporter trackExporter, Track[] tracks) { + this.myTracksProviderUtils = myTracksProviderUtils; + this.trackExporter = trackExporter; + this.tracks = tracks; + } + + @Override + public boolean isSuccess() { + return trackExporter.isSuccess() && super.isSuccess(); + } + + @Override + public void stopWriteTrack() { + trackExporter.stopWriteTrack(); + super.stopWriteTrack(); + } + + @Override + void performWrite(OutputStream outputStream) throws InterruptedException, IOException { + ZipOutputStream zipOutputStream = null; + try { + zipOutputStream = new ZipOutputStream(outputStream); + + // Add kml file + ZipEntry zipEntry = new ZipEntry(KML_FILE_NAME); + zipOutputStream.putNextEntry(zipEntry); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + trackExporter.writeTrack(byteArrayOutputStream); + zipOutputStream.write(byteArrayOutputStream.toByteArray()); + zipOutputStream.closeEntry(); + + // Add photos + addImages(zipOutputStream); + } finally { + if (zipOutputStream != null) { + zipOutputStream.close(); + } + } + } + + private void addImages(ZipOutputStream zipOutputStream) throws IOException { + for (Track track : tracks) { + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1); + if (cursor != null && cursor.moveToFirst()) { + /* + * Yes, this will skip the first waypoint and that is intentional as + * the first waypoint holds the stats for the track. + */ + while (cursor.moveToNext()) { + Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); + String photoUrl = waypoint.getPhotoUrl(); + if (photoUrl != null && !photoUrl.equals("")) { + addImage(zipOutputStream, photoUrl); + } + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + + private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException { + Uri uri = Uri.parse(photoUrl); + ZipEntry zipEntry = new ZipEntry( + KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment()); + zipOutputStream.putNextEntry(zipEntry); + + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(new File(uri.getPath())); + byte[] buffer = new byte[BUFFER_SIZE]; + int byteCount = 0; + while ((byteCount = fileInputStream.read(buffer)) != -1) { + zipOutputStream.write(buffer, 0, byteCount); + } + } finally { + if (fileInputStream != null) { + fileInputStream.close(); + } + } + zipOutputStream.closeEntry(); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java index c2d45c2ef..6aa75d652 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java @@ -99,7 +99,7 @@ public class SaveActivity extends Activity { saveAsyncTask = (SaveAsyncTask) retained; saveAsyncTask.setActivity(this); } else { - saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, directory); + saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, directory, playTrack); saveAsyncTask.execute(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java index 80c25313b..f6b687f20 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java @@ -34,7 +34,6 @@ import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.OutputStream; /** * Async Task to save tracks to the external storage. @@ -49,11 +48,12 @@ public class SaveAsyncTask extends AsyncTask { private final TrackFileFormat trackFileFormat; private final long[] trackIds; private final File directory; + private final boolean playTrack; private final Context context; private final MyTracksProviderUtils myTracksProviderUtils; private WakeLock wakeLock; - private FileTrackExporter fileTrackExporter; + private TrackExporter trackExporter; // true if the AsyncTask has completed private boolean completed; @@ -75,13 +75,15 @@ public class SaveAsyncTask extends AsyncTask { * @param trackIds the track ids to save. To save all, set to size 1 with * trackIds[0] == -1L * @param directory the directory to save to + * @param playTrack true to play track */ - public SaveAsyncTask( - SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds, File directory) { + public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds, + File directory, boolean playTrack) { this.saveActivity = saveActivity; this.trackFileFormat = trackFileFormat; this.trackIds = trackIds; this.directory = directory; + this.playTrack = playTrack; context = saveActivity.getApplicationContext(); myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); @@ -164,8 +166,8 @@ public class SaveAsyncTask extends AsyncTask { @Override protected void onCancelled() { - if (fileTrackExporter != null) { - fileTrackExporter.stopWriteTrack(); + if (trackExporter != null) { + trackExporter.stopWriteTrack(); } } @@ -179,16 +181,18 @@ public class SaveAsyncTask extends AsyncTask { return false; } Track track = tracks[0]; + boolean useKmz = !playTrack && trackFileFormat == TrackFileFormat.KML; + String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension(); + // Make sure the file doesn't exist yet (possibly by changing the filename) - String fileName = FileUtils.buildUniqueFileName( - directory, track.getName(), trackFileFormat.getExtension()); + String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension); if (fileName == null) { Log.d(TAG, "Unable to get a unique filename for " + track.getName()); return false; } - fileTrackExporter = new FileTrackExporter( - context, myTracksProviderUtils, tracks, trackFileFormat, new TrackExporterListener() { + FileTrackExporter fileTrackExporter = new FileTrackExporter(myTracksProviderUtils, tracks, + trackFileFormat, context, useKmz, new TrackExporterListener() { @Override public void onProgressUpdate(int number, int max) { @@ -202,24 +206,27 @@ public class SaveAsyncTask extends AsyncTask { } }); - File file = null; + trackExporter = useKmz ? new KmzTrackExporter(myTracksProviderUtils, fileTrackExporter, tracks) + : fileTrackExporter; + + File file = new File(directory, fileName); + FileOutputStream fileOutputStream; try { - file = new File(directory, fileName); - OutputStream outputStream = new FileOutputStream(file); - fileTrackExporter.writeTrack(outputStream); + fileOutputStream = new FileOutputStream(file); } catch (FileNotFoundException e) { - Log.d(TAG, "File not found " + fileName, e); + Log.e(TAG, "Unable to open file " + file.getName(), e); return false; } + trackExporter.writeTrack(fileOutputStream); - if (fileTrackExporter.isSuccess()) { + if (trackExporter.isSuccess()) { savedPath = file.getAbsolutePath(); } else { if (!file.delete()) { Log.w(TAG, "Failed to delete file " + file.getAbsolutePath()); } } - return fileTrackExporter.isSuccess(); + return trackExporter.isSuccess(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFileFormat.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFileFormat.java index b5e6db5f1..6b5c2c311 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFileFormat.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFileFormat.java @@ -12,25 +12,25 @@ import java.util.Locale; public enum TrackFileFormat implements Parcelable { KML { @Override - TrackWriter newTrackWriter(Context context) { - return new KmlTrackWriter(context); + TrackWriter newTrackWriter(Context context, boolean inZip) { + return new KmlTrackWriter(context, inZip); } }, GPX { @Override - TrackWriter newTrackWriter(Context context) { + TrackWriter newTrackWriter(Context context, boolean inZip) { return new GpxTrackWriter(context); } }, CSV { @Override - public TrackWriter newTrackWriter(Context context) { + public TrackWriter newTrackWriter(Context context, boolean inZip) { return new CsvTrackWriter(context); } }, TCX { @Override - public TrackWriter newTrackWriter(Context context) { + public TrackWriter newTrackWriter(Context context, boolean inZip) { return new TcxTrackWriter(context); } }; @@ -60,7 +60,7 @@ public enum TrackFileFormat implements Parcelable { /** * Creates a new track writer for the format. */ - abstract TrackWriter newTrackWriter(Context context); + abstract TrackWriter newTrackWriter(Context context, boolean inZip); /** * Returns the mime type for each format. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sync/SyncUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/sync/SyncUtils.java index fe91e411e..371eafcdc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sync/SyncUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sync/SyncUtils.java @@ -442,7 +442,7 @@ public class SyncUtils { java.io.File file = new java.io.File(directory, FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension())); FileTrackExporter fileTrackExporter = new FileTrackExporter( - context, myTracksProviderUtils, new Track[] {track}, trackFileFormat, null); + myTracksProviderUtils, new Track[] { track }, trackFileFormat, context, false, null); fileTrackExporter.writeTrack(new FileOutputStream(file)); if (fileTrackExporter.isSuccess()) { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java index 21a42b9aa..71ddf8eae 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java @@ -54,7 +54,8 @@ public class KmlTrackWriterTest extends TrackWriterTest { } public void testXmlOutput() throws Exception { - KmlTrackWriter kmlTrackWriter = new KmlTrackWriter(getContext(), new FakeDescriptionGenerator()); + KmlTrackWriter kmlTrackWriter = new KmlTrackWriter( + getContext(), false, new FakeDescriptionGenerator()); String result = writeTrack(kmlTrackWriter); Document doc = parseXmlDocument(result);