Refactor code.

TrackWriter = handle writing a track to an outputstream
SaveAsyncTask = handle saving to external storage
SaveActivity = handle all UI messages when saving to external storage
This commit is contained in:
Jimmy Shih
2013-01-08 15:27:46 -08:00
parent 986295c343
commit 67819ef7f5
16 changed files with 839 additions and 1097 deletions
@@ -56,7 +56,7 @@ public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.externalFileBackup = new ExternalFileBackup(backupActivity);
success = false;
completed = false;
messageId = R.string.external_storage_save_error;
messageId = R.string.settings_backup_now_error;
}
/**
@@ -86,13 +86,13 @@ public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
messageId = R.string.external_storage_save_error_create_dir;
messageId = R.string.external_storage_not_writeable;
return false;
}
try {
externalFileBackup.writeToDefaultFile();
messageId = R.string.external_storage_save_success;
messageId = R.string.settings_backup_now_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
@@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
success = false;
completed = false;
messageId = R.string.restore_error;
messageId = R.string.settings_backup_restore_error;
}
/**
@@ -85,7 +85,7 @@ public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected Boolean doInBackground(Void... params) {
try {
externalFileBackup.restoreFromDate(date);
messageId = R.string.restore_success;
messageId = R.string.settings_backup_restore_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
@@ -61,7 +61,7 @@ public class ImportActivity extends Activity {
// the path on the external storage to import
private String path;
// the number of successfully imported files
// the number of files successfully imported
private int successCount;
// the number of files to import
@@ -137,29 +137,35 @@ public class ImportActivity extends Activity {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
}, path);
return progressDialog;
case DIALOG_RESULT_ID:
final boolean success;
String message;
if (totalCount == 0) {
message = getString(R.string.import_no_file, path);
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount && totalCount > 0) {
success = true;
message = getString(R.string.import_success, totalFiles, path);
} else {
String totalFiles = getResources()
.getQuantityString(R.plurals.importFiles, totalCount, totalCount);
message = getString(R.string.import_success, successCount, totalFiles, path);
success = false;
message = getString(R.string.import_error, successCount, totalFiles, path);
}
return new AlertDialog.Builder(this).setCancelable(true)
return new AlertDialog.Builder(this).setCancelable(true).setIcon(
success ? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!importAll && trackId != -1L) {
if (success && !importAll && trackId != -1L) {
Intent intent = IntentUtils.newIntent(
ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
@@ -167,9 +173,11 @@ public class ImportActivity extends Activity {
taskStackBuilder.addNextIntent(intent);
taskStackBuilder.startActivities();
}
dialog.dismiss();
finish();
}
}).create();
}).setTitle(success ? R.string.generic_success_title : R.string.generic_error_title)
.create();
default:
return null;
}
@@ -179,7 +187,7 @@ public class ImportActivity extends Activity {
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of files successfully imported
* @param aTotalCount the total number of files to import
* @param aTotalCount the number of files to import
* @param aTrackId the last successfully imported track id
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) {
@@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
@@ -42,6 +43,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final boolean importAll;
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
private WakeLock wakeLock;
// true if the AsyncTask has completed
@@ -70,7 +72,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.importAll = importAll;
this.trackFileFormat = trackFileFormat;
this.path = path;
context = importActivity.getApplicationContext();
// Get the wake lock if not recording or paused
if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT || PreferencesUtils.getBoolean(
@@ -154,9 +157,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*/
private boolean importFile(final File file) {
try {
AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter(
importActivity)
: new GpxImporter(importActivity);
AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter(context)
: new GpxImporter(context);
long trackIds[] = importer.importFile(new FileInputStream(file));
int length = trackIds.length;
if (length > 0) {
@@ -17,6 +17,7 @@
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
@@ -28,12 +29,13 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import java.io.File;
/**
* An activity for saving tracks to the SD card. If saving a specific track,
* option to save it to a temp directory and play the track afterward.
* An activity for saving tracks to the external storage. If saving a specific
* track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
@@ -57,17 +59,18 @@ public class SaveActivity extends Activity {
private TrackFileFormat trackFileFormat;
private long trackId;
private boolean playTrack;
private String directoryName;
private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog;
// result from the AsyncTask
private boolean success;
// message id from the AsyncTask
private int messageId;
// saved file path from the AsyncTask
// the number of tracks successfully saved
private int successCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
@Override
@@ -79,12 +82,29 @@ public class SaveActivity extends Activity {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writeable, Toast.LENGTH_LONG).show();
finish();
return;
}
directoryName = playTrack ? FileUtils.buildExternalDirectoryPath(
trackFileFormat.getExtension(), "tmp")
: FileUtils.buildExternalDirectoryPath(trackFileFormat.getExtension());
File directory = new File(directoryName);
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writeable, Toast.LENGTH_LONG).show();
finish();
return;
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackId, playTrack);
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackId, directory);
saveAsyncTask.execute();
}
}
@@ -99,37 +119,43 @@ public class SaveActivity extends Activity {
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.external_storage_save_progress_message,
new DialogInterface.OnCancelListener() {
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.save_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
});
}, directoryName);
return progressDialog;
case DIALOG_RESULT_ID:
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setCancelable(true)
.setIcon(success
? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
.setMessage(messageId)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
boolean success;
String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
if (successCount == totalCount && totalCount > 0) {
success = true;
message = getString(R.string.save_success, totalTracks, directoryName);
} else {
success = false;
message = getString(R.string.save_error, successCount, totalTracks, directoryName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true).setIcon(
success ? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
})
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
})
.setTitle(success ? R.string.generic_success_title : R.string.generic_error_title);
}).setTitle(success ? R.string.generic_success_title : R.string.generic_error_title);
if (success && trackId != -1L && !playTrack && savedPath != null) {
builder.setNegativeButton(
@@ -152,19 +178,19 @@ public class SaveActivity extends Activity {
/**
* Invokes when the associated AsyncTask completes.
*
* @param isSuccess true if the AsyncTask is successful
* @param aMessageId the id of the AsyncTask message
* @param aSavedPath the path of the saved file
*
* @param aSuccessCount the number of tracks successfully saved
* @param aTotalCount the number of tracks to save
* @param aSavedPath the last successfully saved path
*/
public void onAsyncTaskCompleted(boolean isSuccess, int aMessageId, String aSavedPath) {
this.success = isSuccess;
this.messageId = aMessageId;
this.savedPath = aSavedPath;
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
successCount = aSuccessCount;
totalCount = aTotalCount;
savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID);
if (success && playTrack && savedPath != null) {
Intent intent = new Intent()
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
if (successCount == 1 && totalCount == 1 && playTrack && savedPath != null) {
Intent intent = new Intent().addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(GOOGLE_EARTH_TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID)
.setClassName(GOOGLE_EARTH_PACKAGE, GOOGLE_EARTH_CLASS)
.setDataAndType(Uri.fromFile(new File(savedPath)), GOOGLE_EARTH_KML_MIME_TYPE);
@@ -184,7 +210,7 @@ public class SaveActivity extends Activity {
/**
* Sets the progress dialog value.
*
*
* @param number the number of points saved
* @param max the maximum number of points
*/
@@ -17,6 +17,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.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -30,9 +31,12 @@ import android.os.PowerManager.WakeLock;
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 SD card.
* Async Task to save tracks to the external storage.
*
* @author Jimmy Shih
*/
@@ -43,42 +47,41 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private SaveActivity saveActivity;
private final TrackFileFormat trackFileFormat;
private final long trackId;
private final boolean useTempDir;
private final File directory;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private TrackWriter trackWriter;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
// the number of tracks successfully saved
private int successCount;
// saved file path to return to the activity
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
/**
* Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task.
* @param trackFileFormat the track format
* @param trackId the track id
* @param useTempDir true to use the temp directory
* @param saveActivity the activity currently associated with this task
* @track id the track id to save, -1L to save all tracks
* @param trackFileFormat the track file format
* @param directory the directory to save to
*/
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long trackId,
boolean useTempDir) {
public SaveAsyncTask(
SaveActivity saveActivity, TrackFileFormat trackFileFormat, long trackId, File directory) {
this.saveActivity = saveActivity;
this.trackFileFormat = trackFileFormat;
this.trackId = trackId;
this.useTempDir = useTempDir;
this.directory = directory;
context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(saveActivity);
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
// Get the wake lock if not recording or paused
if (PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
@@ -86,11 +89,10 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT)) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
trackWriter = null;
success = false;
completed = false;
messageId = R.string.external_storage_save_error;
successCount = 0;
totalCount = 0;
savedPath = null;
}
@@ -102,7 +104,7 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
if (completed && saveActivity != null) {
saveActivity.onAsyncTaskCompleted(success, messageId, savedPath);
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@@ -117,7 +119,13 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected Boolean doInBackground(Void... params) {
try {
if (trackId != -1L) {
return saveOneTrack(trackId, true);
totalCount = 1;
if (saveOneTrack(trackId)) {
successCount = 1;
return true;
} else {
return false;
}
} else {
return saveAllTracks();
}
@@ -129,35 +137,79 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onCancelled() {
if (trackWriter != null) {
trackWriter.stopWriteTrack();
}
}
/**
* Saves one track.
*
* @param id the track id
* @param updateSavingProgress true to update the saving progress
*/
private Boolean saveOneTrack(long id, final boolean updateSavingProgress) {
trackWriter = TrackWriterFactory.newWriter(
context, myTracksProviderUtils, id, trackFileFormat);
if (trackWriter == null) {
Log.e(TAG, "Track writer is null");
private Boolean saveOneTrack(long id) {
Track track = myTracksProviderUtils.getTrack(id);
if (track == null) {
Log.d(TAG, "No track for " + id);
return false;
}
if (useTempDir) {
String dirName = FileUtils.buildExternalDirectoryPath(trackFileFormat.getExtension(), "tmp");
trackWriter.setDirectory(new File(dirName));
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = FileUtils.buildUniqueFileName(
directory, track.getName(), trackFileFormat.getExtension());
if (fileName == null) {
Log.d(TAG, "Unable to get a unique filename for " + track.getName());
return false;
}
trackWriter.setOnWriteListener(new TrackWriter.OnWriteListener() {
@Override
public void onWrite(int number, int max) {
// Update the progress dialog once every 500 points
if (updateSavingProgress && number % 500 == 0) {
publishProgress(number, max);
}
trackWriter = new TrackWriter(
context, myTracksProviderUtils, track, trackFileFormat, new TrackWriter.OnWriteListener() {
@Override
public void onWrite(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
*/
if (trackId != -1L && number % 500 == 0) {
publishProgress(number, max);
}
}
});
File file = null;
try {
file = new File(directory, fileName);
OutputStream outputStream = new FileOutputStream(file);
trackWriter.writeTrack(outputStream);
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found " + fileName, e);
return false;
}
if (trackWriter.wasSuccess()) {
savedPath = file.getAbsolutePath();
} else {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
});
trackWriter.writeTrack();
messageId = trackWriter.getErrorMessage();
savedPath = trackWriter.getAbsolutePath();
}
return trackWriter.wasSuccess();
}
@@ -169,27 +221,21 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try {
cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID);
if (cursor == null) {
messageId = R.string.external_storage_save_error_no_track;
return false;
}
int count = cursor.getCount();
if (count == 0) {
messageId = R.string.external_storage_save_error_no_track;
return false;
}
totalCount = cursor.getCount();
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
for (int i = 0; i < count; i++) {
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
long id = cursor.getLong(idIndex);
if (!saveOneTrack(id, false)) {
return false;
}
publishProgress(i + 1, count);
if (saveOneTrack(id)) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
messageId = R.string.external_storage_save_success;
return true;
} finally {
if (cursor != null) {
@@ -197,26 +243,4 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(success, messageId, savedPath);
}
}
@Override
protected void onCancelled() {
if (trackWriter != null) {
trackWriter.stopWriteTrack();
}
}
}
@@ -13,65 +13,267 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import java.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;
/**
* 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.
*
* Track Writer for writing a track to an {@link OutputStream}.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public interface TrackWriter {
public class TrackWriter {
/** This listener is used to signal track writes. */
/**
* Listener for when a track location is written.
*/
public interface OnWriteListener {
/**
* This method is invoked whenever a location within a track is written.
* When a track location is written.
*
* @param number the location number
* @param max the maximum number of locations, for calculation of
* completion percentage
* @param max the maximum number of locations, for calculation of completion
* percentage
*/
public void onWrite(int number, int max);
}
/**
* Sets a listener to be invoked for each location writer.
*/
void setOnWriteListener(OnWriteListener onWriteListener);
private final static String TAG = TrackWriter.class.getSimpleName();
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track track;
private final TrackFormatWriter trackFormatWriter;
private final OnWriteListener onWriteListener;
private Thread writeThread;
private boolean success = false;
/**
* Sets a custom directory where the file will be written.
* Constructor.
*
* @param context the context
* @param myTracksProviderUtils the my tracks provider utils
* @param track the track
* @param trackFileFormat the track file format
* @param onWriteListener the on write listener
*/
void setDirectory(File directory);
/**
* Returns the absolute path to the file which was created.
*/
String getAbsolutePath();
/**
* Writes the given track id to the SD card.
* This is blocking.
*/
void writeTrack();
/**
* Stop any in-progress writes
*/
void stopWriteTrack();
public TrackWriter(Context context, MyTracksProviderUtils myTracksProviderUtils, Track track,
TrackFileFormat trackFileFormat, OnWriteListener onWriteListener) {
this(myTracksProviderUtils, track, trackFileFormat.newFormatWriter(context), onWriteListener);
}
@VisibleForTesting
public TrackWriter(MyTracksProviderUtils myTracksProviderUtils, Track track,
TrackFormatWriter trackFormatWriter, OnWriteListener onWriteListener) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.track = track;
this.trackFormatWriter = trackFormatWriter;
this.onWriteListener = onWriteListener;
}
/**
* Returns true if the write completed successfully.
*/
boolean wasSuccess();
public boolean wasSuccess() {
return success;
}
/**
* Returns the error message (if any) generated by a writer failure.
* Writes the given track to the output stream.
*
* @param outputStream the output stream.
*/
int getErrorMessage();
public void writeTrack(final OutputStream outputStream) {
writeThread = new Thread() {
@Override
public void run() {
try {
trackFormatWriter.prepare(track, outputStream);
trackFormatWriter.writeHeader();
writeWaypoints();
writeLocations();
trackFormatWriter.writeFooter();
trackFormatWriter.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() {
/*
* 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.
*/
Cursor cursor = myTracksProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
boolean hasWaypoints = false;
if (cursor != null) {
try {
if (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) {
trackFormatWriter.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
trackFormatWriter.writeWaypoint(waypoint);
}
}
} finally {
cursor.close();
}
}
if (hasWaypoints) {
trackFormatWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations() 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();
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
trackFormatWriter.writeBeginTrack(locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackFormatWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackFormatWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackFormatWriter.writeLocation(location);
if (onWriteListener != null) {
onWriteListener.onWrite(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackFormatWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteSegment) {
trackFormatWriter.writeCloseSegment();
wroteSegment = false;
}
if (wroteTrack) {
trackFormatWriter.writeEndTrack(locationFactory.lastLocation);
} else {
// Write an empty track
trackFormatWriter.writeBeginTrack(null);
trackFormatWriter.writeEndTrack(null);
}
} finally {
iterator.close();
}
}
/**
* 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();
}
}
}
}
@@ -1,57 +0,0 @@
/*
* 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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import android.content.Context;
import android.util.Log;
/**
* A factory to produce track writers for any format.
*
* @author Rodrigo Damazio
*/
public class TrackWriterFactory {
private TrackWriterFactory() {}
/**
* Creates a new track writer to write the track with the given ID.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param trackId the ID of the track to be written
* @param format the output format to write in
* @return the new track writer
*/
public static TrackWriter newWriter(
Context context, MyTracksProviderUtils providerUtils, long trackId, TrackFileFormat format) {
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return null;
}
TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriterImpl(context, providerUtils, track, writer);
}
}
@@ -1,373 +0,0 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
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;
/**
* This class exports tracks to the SD card. It 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
*/
class TrackWriterImpl implements TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private boolean success = false;
private int errorMessage = R.string.external_storage_save_error;
private File directory = null;
private File file = null;
private OnWriteListener onWriteListener;
private Thread writeThread;
TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
}
@Override
public void setOnWriteListener(OnWriteListener onWriteListener) {
this.onWriteListener = onWriteListener;
}
@Override
public void setDirectory(File directory) {
this.directory = directory;
}
@Override
public String getAbsolutePath() {
return file == null ? null : file.getAbsolutePath();
}
private void writeTrackAsync() {
writeThread = new Thread() {
@Override
public void run() {
doWriteTrack();
}
};
writeThread.start();
}
@Override
public void writeTrack() {
writeTrackAsync();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Interrupted waiting for write to complete", e);
}
}
private void doWriteTrack() {
// Open the input and output
success = false;
errorMessage = R.string.external_storage_save_error;
if (track != null) {
if (openFile()) {
try {
writeDocument();
} catch (InterruptedException e) {
Log.i(Constants.TAG, "The track write was interrupted");
if (file != null && !file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
success = false;
errorMessage = R.string.external_storage_canceled;
}
}
}
}
public void stopWriteTrack() {
if (writeThread != null && writeThread.isAlive()) {
Log.i(Constants.TAG, "Attempting to stop track write");
writeThread.interrupt();
try {
writeThread.join();
Log.i(Constants.TAG, "Track write stopped");
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Failed to wait for writer to stop", e);
}
}
}
@Override
public int getErrorMessage() {
return errorMessage;
}
@Override
public boolean wasSuccess() {
return success;
}
/*
* Helper methods:
* ===============
*/
/**
* 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 " + track.getName());
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.external_storage_save_error;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (!FileUtils.isExternalStorageWriteable()) {
errorMessage = R.string.external_storage_not_writeable;
return false;
}
if (directory == null) {
String dirName =
FileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!FileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.external_storage_save_error_create_dir;
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.getWaypointCursor(trackId, -1L,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
boolean hasWaypoints = false;
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()) {
if (!hasWaypoints) {
writer.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
}
} finally {
cursor.close();
}
}
if (hasWaypoints) {
writer.writeEndWaypoints();
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() throws InterruptedException {
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.external_storage_save_success;
}
private void writeLocations() throws InterruptedException {
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.getTrackPointLocationIterator(track.getId(), -1L, false,
locationFactory);
try {
int pointNumber = 0;
while (it.hasNext()) {
Location location = it.next();
if (Thread.interrupted()) {
throw new InterruptedException();
}
pointNumber++;
boolean isValid = LocationUtils.isValidLocation(location);
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(location);
if (onWriteListener != null) {
onWriteListener.onWrite(pointNumber, track.getNumberOfPoints());
}
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
} else {
// Writes an empty track
writer.writeBeginTrack(null);
writer.writeEndTrack(null);
}
} finally {
it.close();
}
}
}