Code cleanup: Add TrackExporter interface

This commit is contained in:
Jimmy Shih
2013-08-28 15:25:55 -07:00
parent ee7376919e
commit b8bd8d62ce
6 changed files with 372 additions and 295 deletions
@@ -0,0 +1,278 @@
/*
* 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 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.LocationUtils;
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;
/**
* Track Writer for writing tracks to an {@link OutputStream}.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class FileTrackExporter implements TrackExporter {
private final static String TAG = FileTrackExporter.class.getSimpleName();
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track[] tracks;
private final TrackWriter trackWriter;
private final TrackExporterListener trackExporterListener;
private Thread writeThread;
private boolean success = false;
/**
* Constructor.
*
* @param context the context
* @param myTracksProviderUtils the my tracks provider utils
* @param tracks the tracks
* @param trackFileFormat the track file format
* @param trackExporterListener the track export listener
*/
public FileTrackExporter(Context context, MyTracksProviderUtils myTracksProviderUtils,
Track[] tracks, TrackFileFormat trackFileFormat,
TrackExporterListener trackExporterListener) {
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context),
trackExporterListener);
}
@VisibleForTesting
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
this.trackExporterListener = trackExporterListener;
}
@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;
}
}
}
/**
* Writes the waypoints.
*/
private void writeWaypoints(Track track) {
/*
* TODO: Stream through the 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.
*/
boolean hasWaypoints = false;
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
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()) {
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
try {
int locationNumber = 0;
while (iterator.hasNext()) {
Location location = iterator.next();
setLocationTime(location, offset);
if (Thread.interrupted()) {
throw new InterruptedException();
}
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
if (wroteTrack) {
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
iterator.close();
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private 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 tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
}
}
}
@@ -19,6 +19,7 @@ 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.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackExporter.TrackExporterListener;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -52,7 +53,7 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private TrackExporter trackExporter;
private FileTrackExporter fileTrackExporter;
// true if the AsyncTask has completed
private boolean completed;
@@ -163,8 +164,8 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onCancelled() {
if (trackExporter != null) {
trackExporter.stopWriteTrack();
if (fileTrackExporter != null) {
fileTrackExporter.stopWriteTrack();
}
}
@@ -186,10 +187,11 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
return false;
}
trackExporter = new TrackExporter(
context, myTracksProviderUtils, tracks, trackFileFormat, new TrackExporter.OnWriteListener() {
fileTrackExporter = new FileTrackExporter(
context, myTracksProviderUtils, tracks, trackFileFormat, new TrackExporterListener() {
@Override
public void onWrite(int number, int max) {
public void onProgressUpdate(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
@@ -204,20 +206,20 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try {
file = new File(directory, fileName);
OutputStream outputStream = new FileOutputStream(file);
trackExporter.writeTrack(outputStream);
fileTrackExporter.writeTrack(outputStream);
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found " + fileName, e);
return false;
}
if (trackExporter.wasSuccess()) {
if (fileTrackExporter.isSuccess()) {
savedPath = file.getAbsolutePath();
} else {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
}
return trackExporter.wasSuccess();
return fileTrackExporter.isSuccess();
}
/**
@@ -16,284 +16,47 @@
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.LocationUtils;
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;
/**
* Track Writer for writing tracks to an {@link OutputStream}.
* Track exporting for exporting track to an {@link OutputStream}.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
* @author Jimmy Shih
*/
public class TrackExporter {
public interface TrackExporter {
/**
* Listener for when a track location is written.
*/
public interface OnWriteListener {
/**
* When a track location is written.
*
* @param number the location number
* @param max the maximum number of locations in a track, for calculation of completion
* percentage
*/
public void onWrite(int number, int max);
}
private final static String TAG = TrackExporter.class.getSimpleName();
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track[] tracks;
private final TrackWriter trackWriter;
private final OnWriteListener onWriteListener;
private Thread writeThread;
private boolean success = false;
/**
* Constructor.
*
* @param context the context
* @param myTracksProviderUtils the my tracks provider utils
* @param tracks the tracks
* @param trackFileFormat the track file format
* @param onWriteListener the on write listener
*/
public TrackExporter(Context context, MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
TrackFileFormat trackFileFormat, OnWriteListener onWriteListener) {
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context), onWriteListener);
}
@VisibleForTesting
public TrackExporter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
TrackWriter trackWriter, OnWriteListener onWriteListener) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
this.onWriteListener = onWriteListener;
}
/**
* Returns true if the write completed successfully.
*/
public boolean wasSuccess() {
return success;
}
/**
* Writes the given track to the output stream.
*
* @param outputStream the output stream.
*/
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;
}
}
/**
* Stops any in-progress writes.
*/
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;
}
}
}
/**
* Writes the waypoints.
*/
private void writeWaypoints(Track track) {
/*
* TODO: Stream through the 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.
*/
boolean hasWaypoints = false;
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
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()) {
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
try {
int locationNumber = 0;
while (iterator.hasNext()) {
Location location = iterator.next();
setLocationTime(location, offset);
if (Thread.interrupted()) {
throw new InterruptedException();
}
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (onWriteListener != null) {
onWriteListener.onWrite(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
if (wroteTrack) {
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
iterator.close();
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
* Listener for {@link TrackExporter} progress.
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
interface TrackExporterListener {
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
}
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* completion percentage
*/
void onProgressUpdate(int number, int max);
}
/**
* Write track to an output stream
*
* @param outputStream the output stream
*/
void writeTrack(OutputStream outputStream);
/**
* Stops {@link #writeTrack(OutputStream)}.
*/
void stopWriteTrack();
/**
* Returns true if {@link #writeTrack(OutputStream)} completed successfully.
*/
boolean isSuccess();
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010 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;
/**
* Listener for {@link TrackExporter} progress.
*
* @author Jimmy Shih
*/
public interface TrackExporterListener {
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* completion percentage
*/
public void onProgressUpdate(int number, int max);
}
@@ -20,8 +20,8 @@ import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.FileTrackExporter;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.TrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
@@ -441,11 +441,11 @@ public class SyncUtils {
java.io.File file = new java.io.File(directory,
FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension()));
TrackExporter trackExporter = new TrackExporter(
FileTrackExporter fileTrackExporter = new FileTrackExporter(
context, myTracksProviderUtils, new Track[] {track}, trackFileFormat, null);
trackExporter.writeTrack(new FileOutputStream(file));
if (trackExporter.wasSuccess()) {
fileTrackExporter.writeTrack(new FileOutputStream(file));
if (fileTrackExporter.isSuccess()) {
return file;
}
Log.d(TAG, "Unable to get file for track " + track.getName());
@@ -7,7 +7,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackExporter.OnWriteListener;
import com.google.android.apps.mytracks.io.file.TrackExporter.TrackExporterListener;
import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext;
import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
@@ -25,11 +25,11 @@ import org.easymock.IArgumentMatcher;
import org.easymock.IMocksControl;
/**
* Tests for {@link TrackExporter}.
* Tests for {@link FileTrackExporter}.
*
* @author Rodrigo Damazio
*/
public class TrackExporterTest extends AndroidTestCase {
public class FileTrackExporterTest extends AndroidTestCase {
private static final long TRACK_ID = 1234567L;
private static final String TRACK_NAME = "Swimming across the pacific";
@@ -42,7 +42,7 @@ public class TrackExporterTest extends AndroidTestCase {
private TrackWriter trackWriter;
private Track track;
private OutputStream outputStream;
private TrackExporter trackExporter;
private FileTrackExporter fileTrackExporter;
@Override
protected void setUp() throws Exception {
@@ -68,15 +68,16 @@ public class TrackExporterTest extends AndroidTestCase {
track.setId(TRACK_ID);
outputStream = new ByteArrayOutputStream();
OnWriteListener onWriteListener = new OnWriteListener() {
TrackExporterListener trackExporterListener = new TrackExporterListener() {
@Override
public void onWrite(int number, int max) {
public void onProgressUpdate(int number, int max) {
// Safe to ignore
}
};
trackExporter = new TrackExporter(myTracksProviderUtils, new Track[] {track}, trackWriter, onWriteListener);
fileTrackExporter = new FileTrackExporter(
myTracksProviderUtils, new Track[] { track }, trackWriter, trackExporterListener);
}
@Override
@@ -99,9 +100,9 @@ public class TrackExporterTest extends AndroidTestCase {
trackWriter.close();
mocksControl.replay();
trackExporter.writeTrack(outputStream);
fileTrackExporter.writeTrack(outputStream);
assertTrue(trackExporter.wasSuccess());
assertTrue(fileTrackExporter.isSuccess());
mocksControl.verify();
}
@@ -131,9 +132,9 @@ public class TrackExporterTest extends AndroidTestCase {
trackWriter.close();
mocksControl.replay();
trackExporter.writeTrack(outputStream);
fileTrackExporter.writeTrack(outputStream);
assertTrue(trackExporter.wasSuccess());
assertTrue(fileTrackExporter.isSuccess());
mocksControl.verify();
}
@@ -196,9 +197,9 @@ public class TrackExporterTest extends AndroidTestCase {
trackWriter.close();
mocksControl.replay();
trackExporter.writeTrack(outputStream);
fileTrackExporter.writeTrack(outputStream);
assertTrue(trackExporter.wasSuccess());
assertTrue(fileTrackExporter.isSuccess());
mocksControl.verify();
}