diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java index e4ee0589c..4b1e7340d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java @@ -1,12 +1,12 @@ /* - * Copyright 2008 Google Inc. - * + * 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 @@ -15,332 +15,54 @@ */ package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.content.MyTracksLocation; -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 com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.util.FileUtils; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.content.Context; -import android.database.Cursor; -import android.location.Location; -import android.util.Log; - import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.OutputStream; /** - * The class which exports tracks to the SD card. - * This writer is format-neutral - it handles creating the output file - * and reading the track to be exported, but requires an instance of + * Implementations of this class export tracks to the SD card. This class is + * intended to be format-neutral - it handles creating the output file and + * reading the track to be exported, but requires an instance of * {@link TrackFormatWriter} to actually format the data. * * @author Sandor Dornbush * @author Rodrigo Damazio */ -public class TrackWriter { - - private final Context context; - private final MyTracksProviderUtils providerUtils; - private final Track track; - private final TrackFormatWriter writer; - private final FileUtils fileUtils; - private Runnable onCompletion = null; - private boolean success = false; - private int errorMessage = -1; - private File directory = null; - private File file = null; - - public TrackWriter(Context context, MyTracksProviderUtils providerUtils, - Track track, TrackFormatWriter writer) { - this.context = context; - this.providerUtils = providerUtils; - this.track = track; - this.writer = writer; - this.fileUtils = new FileUtils(); - } - +public interface TrackWriter { /** * Sets a completion callback. * * @param onCompletion Runnable that will be executed when finished */ - public void setOnCompletion(Runnable onCompletion) { - this.onCompletion = onCompletion; - } + void setOnCompletion(Runnable onCompletion); /** * Sets a custom directory where the file will be written. */ - public void setDirectory(File directory) { - this.directory = directory; - } + void setDirectory(File directory); - public String getAbsolutePath() { - return file.getAbsolutePath(); - } + /** + * Returns the absolute path to the file which was created. + */ + String getAbsolutePath(); /** * Writes the given track id to the SD card. * This is non-blocking. */ - public void writeTrackAsync() { - Thread t = new Thread() { - @Override - public void run() { - writeTrack(); - } - }; - t.start(); - } + void writeTrackAsync(); /** * Writes the given track id to the SD card. * This is blocking. */ - public void writeTrack() { - // Open the input and output - success = false; - errorMessage = R.string.error_track_does_not_exist; - if (track != null) { - if (openFile()) { - writeDocument(); - } - } - finished(); - } - - public int getErrorMessage() { - return errorMessage; - } - - public boolean wasSuccess() { - return success; - } - - /* - * Helper methods: - * =============== - */ - - private void finished() { - if (onCompletion != null) { - runOnUiThread(onCompletion); - return; - } - } + void writeTrack(); /** - * Runs the given runnable in the UI thread. + * Returns true if the write completed successfully. */ - protected void runOnUiThread(Runnable runnable) { - if (context instanceof Activity) { - ((Activity) context).runOnUiThread(runnable); - } - } + boolean wasSuccess(); /** - * Opens the file and prepares the format writer for it. - * - * @return true on success, false otherwise (and errorMessage is set) + * Returns the error message (if any) generated by a writer failure. */ - protected boolean openFile() { - if (!canWriteFile()) { - return false; - } - - // Make sure the file doesn't exist yet (possibly by changing the filename) - String fileName = fileUtils.buildUniqueFileName( - directory, track.getName(), writer.getExtension()); - if (fileName == null) { - Log.e(Constants.TAG, - "Unable to get a unique filename for " + fileName); - return false; - } - - Log.i(Constants.TAG, "Writing track to: " + fileName); - try { - writer.prepare(track, newOutputStream(fileName)); - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "Failed to open output file.", e); - errorMessage = R.string.io_write_failed; - return false; - } - return true; - } - - /** - * Checks and returns whether we're ready to create the output file. - */ - protected boolean canWriteFile() { - if (directory == null) { - String dirName = - fileUtils.buildExternalDirectoryPath(writer.getExtension()); - directory = newFile(dirName); - } - - if (!fileUtils.isSdCardAvailable()) { - Log.i(Constants.TAG, "Could not find SD card."); - errorMessage = R.string.io_no_external_storage_found; - return false; - } - if (!fileUtils.ensureDirectoryExists(directory)) { - Log.i(Constants.TAG, "Could not create export directory."); - errorMessage = R.string.io_create_dir_failed; - return false; - } - - return true; - } - - /** - * Creates a new output stream to write to the given filename. - * - * @throws FileNotFoundException if the file could't be created - */ - protected OutputStream newOutputStream(String fileName) - throws FileNotFoundException { - file = new File(directory, fileName); - return new FileOutputStream(file); - } - - /** - * Creates a new file object for the given path. - */ - protected File newFile(String path) { - return new File(path); - } - - /** - * Writes the waypoints for the given track. - * - * @param trackId the ID of the track to write waypoints for - */ - private void writeWaypoints(long trackId) { - // TODO: Stream through he waypoints in chunks. - // I am leaving the number of waypoints very high which should not be a - // problem because we don't try to load them into objects all at the - // same time. - Cursor cursor = null; - cursor = providerUtils.getWaypointsCursor(trackId, 0, - Constants.MAX_LOADED_WAYPOINTS_POINTS); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - // Yes, this will skip the 1st way point and that is intentional - // as the 1st points holds the stats for the current/last segment. - while (cursor.moveToNext()) { - Waypoint wpt = providerUtils.createWaypoint(cursor); - writer.writeWaypoint(wpt); - } - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - } - - /** - * Does the actual work of writing the track to the now open file. - */ - void writeDocument() { - Log.d(Constants.TAG, "Started writing track."); - writer.writeHeader(); - writeWaypoints(track.getId()); - writeLocations(); - writer.writeFooter(); - writer.close(); - success = true; - Log.d(Constants.TAG, "Done writing track."); - errorMessage = R.string.io_write_finished; - } - - private void writeLocations() { - boolean wroteFirst = false; - boolean segmentOpen = false; - boolean isLastValid = false; - - class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory { - Location currentLocation; - Location lastLocation; - - @Override - public Location createLocation() { - if (currentLocation == null) { - currentLocation = new MyTracksLocation(""); - } - return currentLocation; - } - - public void swapLocations() { - Location tmpLoc = lastLocation; - lastLocation = currentLocation; - currentLocation = tmpLoc; - if (currentLocation != null) { - currentLocation.reset(); - } - } - }; - - TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); - LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false, - locationFactory); - try { - if (!it.hasNext()) { - Log.w(Constants.TAG, "Unable to get any points to write"); - return; - } - while (it.hasNext()) { - Location loc = it.next(); - - boolean isValid = LocationUtils.isValidLocation(loc); - boolean validSegment = isValid && isLastValid; - if (!wroteFirst && validSegment) { - // Found the first two consecutive points which are valid - writer.writeBeginTrack(locationFactory.lastLocation); - wroteFirst = true; - } - - if (validSegment) { - if (!segmentOpen) { - // Start a segment for this point - writer.writeOpenSegment(); - segmentOpen = true; - - // Write the previous point, which we had previously skipped - writer.writeLocation(locationFactory.lastLocation); - } - - // Write the current point - writer.writeLocation(loc); - } else { - if (segmentOpen) { - writer.writeCloseSegment(); - segmentOpen = false; - } - } - - locationFactory.swapLocations(); - isLastValid = isValid; - } - if (segmentOpen) { - writer.writeCloseSegment(); - segmentOpen = false; - } - if (wroteFirst) { - writer.writeEndTrack(locationFactory.lastLocation); - } - } finally { - it.close(); - } - } + int getErrorMessage(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java index 9a0692f88..5aabf6133 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java @@ -106,7 +106,7 @@ public class TrackWriterFactory { MyTracksProviderUtils providerUtils, Track track, TrackFileFormat format) { TrackFormatWriter writer = format.newFormatWriter(context); - return new TrackWriter(context, providerUtils, track, writer); + return new TrackWriterImpl(context, providerUtils, track, writer); } private TrackWriterFactory() { } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java new file mode 100644 index 000000000..c21e9e138 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java @@ -0,0 +1,331 @@ +/* + * Copyright 2008 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.Constants; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.util.FileUtils; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.location.Location; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; + +/** + * @author Sandor Dornbush + * @author Rodrigo Damazio + */ +public class TrackWriterImpl implements TrackWriter { + private final Context context; + private final MyTracksProviderUtils providerUtils; + private final Track track; + private final TrackFormatWriter writer; + private final FileUtils fileUtils; + private Runnable onCompletion = null; + private boolean success = false; + private int errorMessage = -1; + private File directory = null; + private File file = null; + + TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils, + Track track, TrackFormatWriter writer) { + this.context = context; + this.providerUtils = providerUtils; + this.track = track; + this.writer = writer; + this.fileUtils = new FileUtils(); + } + + @Override + public void setOnCompletion(Runnable onCompletion) { + this.onCompletion = onCompletion; + } + + @Override + public void setDirectory(File directory) { + this.directory = directory; + } + + @Override + public String getAbsolutePath() { + return file.getAbsolutePath(); + } + + @Override + public void writeTrackAsync() { + Thread t = new Thread() { + @Override + public void run() { + writeTrack(); + } + }; + t.start(); + } + + @Override + public void writeTrack() { + // Open the input and output + success = false; + errorMessage = R.string.error_track_does_not_exist; + if (track != null) { + if (openFile()) { + writeDocument(); + } + } + finished(); + } + + @Override + public int getErrorMessage() { + return errorMessage; + } + + @Override + public boolean wasSuccess() { + return success; + } + + /* + * Helper methods: + * =============== + */ + + private void finished() { + if (onCompletion != null) { + runOnUiThread(onCompletion); + return; + } + } + + /** + * Runs the given runnable in the UI thread. + */ + protected void runOnUiThread(Runnable runnable) { + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(runnable); + } + } + + /** + * Opens the file and prepares the format writer for it. + * + * @return true on success, false otherwise (and errorMessage is set) + */ + protected boolean openFile() { + if (!canWriteFile()) { + return false; + } + + // Make sure the file doesn't exist yet (possibly by changing the filename) + String fileName = fileUtils.buildUniqueFileName( + directory, track.getName(), writer.getExtension()); + if (fileName == null) { + Log.e(Constants.TAG, + "Unable to get a unique filename for " + fileName); + return false; + } + + Log.i(Constants.TAG, "Writing track to: " + fileName); + try { + writer.prepare(track, newOutputStream(fileName)); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "Failed to open output file.", e); + errorMessage = R.string.io_write_failed; + return false; + } + return true; + } + + /** + * Checks and returns whether we're ready to create the output file. + */ + protected boolean canWriteFile() { + if (directory == null) { + String dirName = + fileUtils.buildExternalDirectoryPath(writer.getExtension()); + directory = newFile(dirName); + } + + if (!fileUtils.isSdCardAvailable()) { + Log.i(Constants.TAG, "Could not find SD card."); + errorMessage = R.string.io_no_external_storage_found; + return false; + } + if (!fileUtils.ensureDirectoryExists(directory)) { + Log.i(Constants.TAG, "Could not create export directory."); + errorMessage = R.string.io_create_dir_failed; + return false; + } + + return true; + } + + /** + * Creates a new output stream to write to the given filename. + * + * @throws FileNotFoundException if the file could't be created + */ + protected OutputStream newOutputStream(String fileName) + throws FileNotFoundException { + file = new File(directory, fileName); + return new FileOutputStream(file); + } + + /** + * Creates a new file object for the given path. + */ + protected File newFile(String path) { + return new File(path); + } + + /** + * Writes the waypoints for the given track. + * + * @param trackId the ID of the track to write waypoints for + */ + private void writeWaypoints(long trackId) { + // TODO: Stream through he waypoints in chunks. + // I am leaving the number of waypoints very high which should not be a + // problem because we don't try to load them into objects all at the + // same time. + Cursor cursor = null; + cursor = providerUtils.getWaypointsCursor(trackId, 0, + Constants.MAX_LOADED_WAYPOINTS_POINTS); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + // Yes, this will skip the 1st way point and that is intentional + // as the 1st points holds the stats for the current/last segment. + while (cursor.moveToNext()) { + Waypoint wpt = providerUtils.createWaypoint(cursor); + writer.writeWaypoint(wpt); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + + /** + * Does the actual work of writing the track to the now open file. + */ + void writeDocument() { + Log.d(Constants.TAG, "Started writing track."); + writer.writeHeader(); + writeWaypoints(track.getId()); + writeLocations(); + writer.writeFooter(); + writer.close(); + success = true; + Log.d(Constants.TAG, "Done writing track."); + errorMessage = R.string.io_write_finished; + } + + private void writeLocations() { + boolean wroteFirst = false; + boolean segmentOpen = false; + boolean isLastValid = false; + + class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory { + Location currentLocation; + Location lastLocation; + + @Override + public Location createLocation() { + if (currentLocation == null) { + currentLocation = new MyTracksLocation(""); + } + return currentLocation; + } + + public void swapLocations() { + Location tmpLoc = lastLocation; + lastLocation = currentLocation; + currentLocation = tmpLoc; + if (currentLocation != null) { + currentLocation.reset(); + } + } + }; + + TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); + LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false, + locationFactory); + try { + if (!it.hasNext()) { + Log.w(Constants.TAG, "Unable to get any points to write"); + return; + } + while (it.hasNext()) { + Location loc = it.next(); + + boolean isValid = LocationUtils.isValidLocation(loc); + boolean validSegment = isValid && isLastValid; + if (!wroteFirst && validSegment) { + // Found the first two consecutive points which are valid + writer.writeBeginTrack(locationFactory.lastLocation); + wroteFirst = true; + } + + if (validSegment) { + if (!segmentOpen) { + // Start a segment for this point + writer.writeOpenSegment(); + segmentOpen = true; + + // Write the previous point, which we had previously skipped + writer.writeLocation(locationFactory.lastLocation); + } + + // Write the current point + writer.writeLocation(loc); + } else { + if (segmentOpen) { + writer.writeCloseSegment(); + segmentOpen = false; + } + } + + locationFactory.swapLocations(); + isLastValid = isValid; + } + if (segmentOpen) { + writer.writeCloseSegment(); + segmentOpen = false; + } + if (wroteFirst) { + writer.writeEndTrack(locationFactory.lastLocation); + } + } finally { + it.close(); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java index c623d9a78..d066ce06b 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java @@ -10,7 +10,7 @@ import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.io.file.TrackFormatWriter; -import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.apps.mytracks.io.file.TrackWriterImpl; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; @@ -35,10 +35,10 @@ import org.easymock.IMocksControl; */ public class TrackWriterTest extends AndroidTestCase { /** - * {@link TrackWriter} subclass which mocks out methods called from - * {@link TrackWriter#openFile}. + * {@link TrackWriterImpl} subclass which mocks out methods called from + * {@link TrackWriterImpl#openFile}. */ - private static final class OpenFileTrackWriter extends TrackWriter { + private static final class OpenFileTrackWriter extends TrackWriterImpl { private final ByteArrayOutputStream stream; private final boolean canWrite; @@ -46,9 +46,9 @@ public class TrackWriterTest extends AndroidTestCase { * Constructor. * * @param stream the stream to return from - * {@link TrackWriter#newOutputStream}, or null to throw a + * {@link TrackWriterImpl#newOutputStream}, or null to throw a * {@link FileNotFoundException} - * @param canWrite the value that {@link TrackWriter#canWriteFile} will + * @param canWrite the value that {@link TrackWriterImpl#canWriteFile} will * return */ private OpenFileTrackWriter(Context context, @@ -80,16 +80,16 @@ public class TrackWriterTest extends AndroidTestCase { } /** - * {@link TrackWriter} subclass which mocks out methods called from - * {@link TrackWriter#writeTrack}. + * {@link TrackWriterImpl} subclass which mocks out methods called from + * {@link TrackWriterImpl#writeTrack}. */ - private final class WriteTracksTrackWriter extends TrackWriter { + private final class WriteTracksTrackWriter extends TrackWriterImpl { private final boolean openResult; /** * Constructor. * - * @param openResult the return value for {@link TrackWriter#openFile} + * @param openResult the return value for {@link TrackWriterImpl#openFile} */ private WriteTracksTrackWriter(Context context, MyTracksProviderUtils providerUtils, Track track, @@ -123,7 +123,7 @@ public class TrackWriterTest extends AndroidTestCase { private Track track; private TrackFormatWriter formatWriter; - private TrackWriter writer; + private TrackWriterImpl writer; private IMocksControl mocksControl; private MyTracksProviderUtils providerUtils; private Factory oldProviderUtilsFactory; @@ -228,7 +228,7 @@ public class TrackWriterTest extends AndroidTestCase { } public void testWriteDocument_emptyTrack() { - writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); + writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); // Set expected mock behavior formatWriter.writeHeader(); @@ -243,7 +243,7 @@ public class TrackWriterTest extends AndroidTestCase { } public void testWriteDocument() { - writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); + writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); final Location[] locs = { new Location("fake0"),