Extract an interface from TrackWriter to ease testing.

This commit is contained in:
Matthew Simmons
2011-04-06 13:49:31 -04:00
parent 375b75c422
commit 3fd729f1a2
4 changed files with 365 additions and 312 deletions
@@ -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 * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,332 +15,54 @@
*/ */
package com.google.android.apps.mytracks.io.file; 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.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/** /**
* The class which exports tracks to the SD card. * Implementations of this class export tracks to the SD card. This class is
* This writer is format-neutral - it handles creating the output file * intended to be format-neutral - it handles creating the output file and
* and reading the track to be exported, but requires an instance of * reading the track to be exported, but requires an instance of
* {@link TrackFormatWriter} to actually format the data. * {@link TrackFormatWriter} to actually format the data.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackWriter { public interface 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();
}
/** /**
* Sets a completion callback. * Sets a completion callback.
* *
* @param onCompletion Runnable that will be executed when finished * @param onCompletion Runnable that will be executed when finished
*/ */
public void setOnCompletion(Runnable onCompletion) { void setOnCompletion(Runnable onCompletion);
this.onCompletion = onCompletion;
}
/** /**
* Sets a custom directory where the file will be written. * Sets a custom directory where the file will be written.
*/ */
public void setDirectory(File directory) { void setDirectory(File directory);
this.directory = 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. * Writes the given track id to the SD card.
* This is non-blocking. * This is non-blocking.
*/ */
public void writeTrackAsync() { void writeTrackAsync();
Thread t = new Thread() {
@Override
public void run() {
writeTrack();
}
};
t.start();
}
/** /**
* Writes the given track id to the SD card. * Writes the given track id to the SD card.
* This is blocking. * This is blocking.
*/ */
public void writeTrack() { 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;
}
}
/** /**
* Runs the given runnable in the UI thread. * Returns true if the write completed successfully.
*/ */
protected void runOnUiThread(Runnable runnable) { boolean wasSuccess();
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
/** /**
* Opens the file and prepares the format writer for it. * Returns the error message (if any) generated by a writer failure.
*
* @return true on success, false otherwise (and errorMessage is set)
*/ */
protected boolean openFile() { int getErrorMessage();
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();
}
}
} }
@@ -106,7 +106,7 @@ public class TrackWriterFactory {
MyTracksProviderUtils providerUtils, MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) { Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context); TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriter(context, providerUtils, track, writer); return new TrackWriterImpl(context, providerUtils, track, writer);
} }
private TrackWriterFactory() { } private TrackWriterFactory() { }
@@ -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();
}
}
}
@@ -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.Waypoint;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; 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.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.services.TrackRecordingServiceTest.MockContext;
import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
@@ -35,10 +35,10 @@ import org.easymock.IMocksControl;
*/ */
public class TrackWriterTest extends AndroidTestCase { public class TrackWriterTest extends AndroidTestCase {
/** /**
* {@link TrackWriter} subclass which mocks out methods called from * {@link TrackWriterImpl} subclass which mocks out methods called from
* {@link TrackWriter#openFile}. * {@link TrackWriterImpl#openFile}.
*/ */
private static final class OpenFileTrackWriter extends TrackWriter { private static final class OpenFileTrackWriter extends TrackWriterImpl {
private final ByteArrayOutputStream stream; private final ByteArrayOutputStream stream;
private final boolean canWrite; private final boolean canWrite;
@@ -46,9 +46,9 @@ public class TrackWriterTest extends AndroidTestCase {
* Constructor. * Constructor.
* *
* @param stream the stream to return from * @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} * {@link FileNotFoundException}
* @param canWrite the value that {@link TrackWriter#canWriteFile} will * @param canWrite the value that {@link TrackWriterImpl#canWriteFile} will
* return * return
*/ */
private OpenFileTrackWriter(Context context, private OpenFileTrackWriter(Context context,
@@ -80,16 +80,16 @@ public class TrackWriterTest extends AndroidTestCase {
} }
/** /**
* {@link TrackWriter} subclass which mocks out methods called from * {@link TrackWriterImpl} subclass which mocks out methods called from
* {@link TrackWriter#writeTrack}. * {@link TrackWriterImpl#writeTrack}.
*/ */
private final class WriteTracksTrackWriter extends TrackWriter { private final class WriteTracksTrackWriter extends TrackWriterImpl {
private final boolean openResult; private final boolean openResult;
/** /**
* Constructor. * Constructor.
* *
* @param openResult the return value for {@link TrackWriter#openFile} * @param openResult the return value for {@link TrackWriterImpl#openFile}
*/ */
private WriteTracksTrackWriter(Context context, private WriteTracksTrackWriter(Context context,
MyTracksProviderUtils providerUtils, Track track, MyTracksProviderUtils providerUtils, Track track,
@@ -123,7 +123,7 @@ public class TrackWriterTest extends AndroidTestCase {
private Track track; private Track track;
private TrackFormatWriter formatWriter; private TrackFormatWriter formatWriter;
private TrackWriter writer; private TrackWriterImpl writer;
private IMocksControl mocksControl; private IMocksControl mocksControl;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
private Factory oldProviderUtilsFactory; private Factory oldProviderUtilsFactory;
@@ -228,7 +228,7 @@ public class TrackWriterTest extends AndroidTestCase {
} }
public void testWriteDocument_emptyTrack() { public void testWriteDocument_emptyTrack() {
writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter);
// Set expected mock behavior // Set expected mock behavior
formatWriter.writeHeader(); formatWriter.writeHeader();
@@ -243,7 +243,7 @@ public class TrackWriterTest extends AndroidTestCase {
} }
public void testWriteDocument() { public void testWriteDocument() {
writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter);
final Location[] locs = { final Location[] locs = {
new Location("fake0"), new Location("fake0"),