diff --git a/MyTracks/res/values/plurals.xml b/MyTracks/res/values/plurals.xml index a2b5bcfe5..db8549cfd 100644 --- a/MyTracks/res/values/plurals.xml +++ b/MyTracks/res/values/plurals.xml @@ -15,13 +15,21 @@ See the License for the specific language governing permissions and limitations under the License. --> - - @string/import_files_zero - @string/import_files_one - @string/import_files_two - @string/import_files_few - @string/import_files_many - @string/import_files_other + + @string/files_zero + @string/files_one + @string/files_two + @string/files_few + @string/files_many + @string/files_other + + + @string/tracks_zero + @string/tracks_one + @string/tracks_two + @string/tracks_few + @string/tracks_many + @string/tracks_other @string/voice_hours_zero diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 16f7a9910..e3f0244bc 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -16,7 +16,7 @@ limitations under the License. --> - + Copyright 2009 Google Inc. @@ -556,7 +556,7 @@ limitations under the License. Segment - + Accept @@ -599,44 +599,60 @@ limitations under the License. - - The operation was canceled. - External storage not available. + translation_description="Error message when the external storage is not writeable."> External storage not writeable. - - Unable to save to external storage. + + + + %1$d files - - Unable to create a directory on the external storage. + + %1$d files - - No track is saved to the external storage. + + 1 file - - Saving to external storage… + + %1$d files - - Finished saving to external storage. + + 2 files + + + 0 files - + Add @@ -703,7 +719,7 @@ limitations under the License. Success - + @@ -744,7 +760,7 @@ limitations under the License. Waiting for GPS signal. - + @@ -961,47 +977,14 @@ limitations under the License. - - %1$d files - - - %1$d files - - - 1 file - - - %1$d files - - - 2 files - - - 0 files + %1$d the number of files successfully imported. E.g., '2'. + %2$s the number of files in the directory. E.g., '2 files'. + %3$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'."> + Imported %1$d of %2$s from %3$s. %1$s does not exist. - - No file found in %1$s. - - Imported %1$d of %2$s from %3$s. + %1$s the number of files. E.g., '2 files'. + %2$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'."> + Imported %1$s from %2$s. @@ -1079,27 +1054,6 @@ limitations under the License. user that the marker will be permanently deleted from the device."> The marker will be permanently deleted from the device. - - Since last split marker: - - - Marker type: %1$s - - - Marker type (e.g. waterfall) - - - Marker type - %1$s (Start) - - You don\'t have any markers. - + Since last split marker: + + + Marker type: %1$s + + + + + Marker type (e.g. waterfall) + + + Marker type + + + + + You don\'t have any markers. + + + - Unable to restore from backup. + + + Saved %1$d of %2$s to %3$s. - - Finished restoring from backup. + + Saving to %1$s… + + + Saved %1$s to %2$s. @@ -1586,6 +1581,25 @@ limitations under the License. Sending + + + Not available while recording a track. + + + Show + + + Switch account + + Backup + + Backup now + + Unable to backup. + Write a full copy of all your tracks and settings to the external storage. + + Finished backup. + + + + Unable to restore from backup. + @@ -1647,6 +1677,10 @@ limitations under the License. explaining that it restores all tracks and settings from the external storage."> Restore all tracks and settings from the external storage. + + Finished restoring from backup. + - - - Not available while recording a track. - - - Show - - - Switch account - - Avg moving pace @@ -2278,6 +2289,19 @@ limitations under the License. + + Track %1$d + + + Recording your track… + + + Chart @@ -2310,6 +2334,8 @@ limitations under the License. translation_description="In the track detail page, the title of the tab showing the stats."> Stats + + @@ -2324,6 +2350,8 @@ limitations under the License. translation_description="The title of the track edit page when saving a new track."> New Track + + @@ -2344,17 +2372,6 @@ limitations under the License. success."> Now recording a new track. - - Track %1$d - - - Recording your track… - + + + %1$d tracks + + + %1$d tracks + + + 1 track + + + %1$d tracks + + + 2 tracks + + + 0 tracks + + @@ -2395,6 +2456,7 @@ limitations under the License. translation_description="Abbreviation for minutes per mile. E.g, '5 min/mi'."> min/mi + diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupAsyncTask.java index 03cca7c64..99b11bbcc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupAsyncTask.java @@ -56,7 +56,7 @@ public class BackupAsyncTask extends AsyncTask { 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 { } 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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java index 665f0e48a..4464667bf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java @@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask { 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 { 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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java index 2cae4923c..b64e26fca 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java index 2e2cba2dc..6f9e20dc7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java @@ -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 { 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 { 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 { */ 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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java index c96f3c83b..dd4336e7e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java @@ -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 */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java index 1af09d29d..ae4f68529 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveAsyncTask.java @@ -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 { 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 { 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 { 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 { 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 { } } + @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 { 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 { } } } - @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(); - } - } } 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 26e1c59c6..a075c584d 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 @@ -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(); + } + } + } } 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 deleted file mode 100644 index f6fa90b5c..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java +++ /dev/null @@ -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); - } -} 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 deleted file mode 100644 index 30c37782b..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java +++ /dev/null @@ -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(); - } - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java deleted file mode 100644 index 60e35bf20..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 java.io.File; - -/** - * A simple, fake {@link TrackWriter} subclass with all methods mocked out. - * Tests are expected to override {@link #writeTrack}. - * - * @author Matthew Simmons - * - */ -public class MockTrackWriter implements TrackWriter { - public OnWriteListener onWriteListener; - - @Override - public void setOnWriteListener(OnWriteListener onWriteListener) { - this.onWriteListener = onWriteListener; - } - - @Override - public void setDirectory(File directory) { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public String getAbsolutePath() { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public void writeTrack() { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public void stopWriteTrack() { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public boolean wasSuccess() { - return false; - } - - @Override - public int getErrorMessage() { - return 0; - } -} 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 1d9fe5e22..9c25bf72a 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 @@ -2,13 +2,12 @@ package com.google.android.apps.mytracks.io.file; -import static org.easymock.EasyMock.expect; - import com.google.android.apps.mytracks.content.MyTracksProvider; 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.TrackWriter.OnWriteListener; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; @@ -19,8 +18,6 @@ import android.test.RenamingDelegatingContext; import android.test.mock.MockContentResolver; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; import java.io.OutputStream; import org.easymock.EasyMock; @@ -28,112 +25,24 @@ import org.easymock.IArgumentMatcher; import org.easymock.IMocksControl; /** - * Tests for the track writer. - * + * Tests for {@link TrackWriter}. + * * @author Rodrigo Damazio */ public class TrackWriterTest extends AndroidTestCase { - /** - * {@link TrackWriterImpl} subclass which mocks out methods called from - * {@link TrackWriterImpl#openFile}. - */ - private static final class OpenFileTrackWriter extends TrackWriterImpl { - private final ByteArrayOutputStream stream; - private final boolean canWrite; - - /** - * Constructor. - * - * @param stream the stream to return from - * {@link TrackWriterImpl#newOutputStream}, or null to throw a - * {@link FileNotFoundException} - * @param canWrite the value that {@link TrackWriterImpl#canWriteFile} will - * return - */ - private OpenFileTrackWriter(Context context, - MyTracksProviderUtils providerUtils, Track track, - TrackFormatWriter writer, ByteArrayOutputStream stream, - boolean canWrite) { - super(context, providerUtils, track, writer); - - this.stream = stream; - this.canWrite = canWrite; - - // The directory is set in the canWriteFile. However, this class - // overwrites canWriteFile, thus needs to set it. - setDirectory(new File("/")); - } - - @Override - protected boolean canWriteFile() { - return canWrite; - } - - @Override - protected OutputStream newOutputStream(String fileName) - throws FileNotFoundException { - assertEquals(FULL_TRACK_NAME, fileName); - - if (stream == null) { - throw new FileNotFoundException(); - } - - return stream; - } - } - - /** - * {@link TrackWriterImpl} subclass which mocks out methods called from - * {@link TrackWriterImpl#writeTrack}. - */ - private final class WriteTracksTrackWriter extends TrackWriterImpl { - private final boolean openResult; - - /** - * Constructor. - * - * @param openResult the return value for {@link TrackWriterImpl#openFile} - */ - private WriteTracksTrackWriter(Context context, - MyTracksProviderUtils providerUtils, Track track, - TrackFormatWriter writer, boolean openResult) { - super(context, providerUtils, track, writer); - this.openResult = openResult; - } - - @Override - protected boolean openFile() { - openFileCalls++; - return openResult; - } - - @Override - void writeDocument() { - writeDocumentCalls++; - } - - @Override - protected void runOnUiThread(Runnable runnable) { - runnable.run(); - } - } private static final long TRACK_ID = 1234567L; - private static final String EXTENSION = "ext"; private static final String TRACK_NAME = "Swimming across the pacific"; - private static final String FULL_TRACK_NAME = - "Swimming across the pacific.ext"; - private Track track; - private TrackFormatWriter formatWriter; - private TrackWriterImpl writer; - private IMocksControl mocksControl; - private MyTracksProviderUtils providerUtils; + private MyTracksProviderUtils myTracksProviderUtils; private Factory oldProviderUtilsFactory; - // State used in specific tests - private int writeDocumentCalls; - private int openFileCalls; + private IMocksControl mocksControl; + + private TrackFormatWriter trackFormatWriter; + private Track track; + private OutputStream outputStream; + private TrackWriter trackWriter; @Override protected void setUp() throws Exception { @@ -143,20 +52,31 @@ public class TrackWriterTest extends AndroidTestCase { RenamingDelegatingContext targetContext = new RenamingDelegatingContext( getContext(), getContext(), "test."); Context context = new MockContext(mockContentResolver, targetContext); - MyTracksProvider provider = new MyTracksProvider(); - provider.attachInfo(context, null); - mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + MyTracksProvider myTracksProvider = new MyTracksProvider(); + myTracksProvider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, myTracksProvider); setContext(context); - providerUtils = MyTracksProviderUtils.Factory.get(context); - oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); + oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance( + myTracksProviderUtils); mocksControl = EasyMock.createStrictControl(); - formatWriter = mocksControl.createMock(TrackFormatWriter.class); - expect(formatWriter.getExtension()).andStubReturn(EXTENSION); + trackFormatWriter = mocksControl.createMock(TrackFormatWriter.class); track = new Track(); track.setName(TRACK_NAME); track.setId(TRACK_ID); + + outputStream = new ByteArrayOutputStream(); + OnWriteListener onWriteListener = new OnWriteListener() { + + @Override + public void onWrite(int number, int max) { + // Safe to ignore + + } + }; + trackWriter = new TrackWriter(myTracksProviderUtils, track, trackFormatWriter, onWriteListener); } @Override @@ -165,226 +85,196 @@ public class TrackWriterTest extends AndroidTestCase { super.tearDown(); } - public void testWriteTrack() { - writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, - formatWriter, true); - - mocksControl.replay(); - writer.writeTrack(); - - assertEquals(1, writeDocumentCalls); - assertEquals(1, openFileCalls); - mocksControl.verify(); - } - - public void testWriteTrack_openFails() { - writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, - formatWriter, false); - - mocksControl.replay(); - writer.writeTrack(); - - assertEquals(0, writeDocumentCalls); - assertEquals(1, openFileCalls); - mocksControl.verify(); - } - - public void testOpenFile() { - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - writer = new OpenFileTrackWriter( - getContext(), providerUtils, track, formatWriter, stream, true); - - formatWriter.prepare(track, stream); - - mocksControl.replay(); - assertTrue(writer.openFile()); - mocksControl.verify(); - } - - public void testOpenFile_cantWrite() { - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - writer = new OpenFileTrackWriter( - getContext(), providerUtils, track, formatWriter, stream, false); - - mocksControl.replay(); - assertFalse(writer.openFile()); - mocksControl.verify(); - } - - public void testOpenFile_streamError() { - writer = new OpenFileTrackWriter( - getContext(), providerUtils, track, formatWriter, null, true); - - mocksControl.replay(); - assertFalse(writer.openFile()); - mocksControl.verify(); - } - - public void testWriteDocument_emptyTrack() throws Exception { - writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); + /** + * Tests write track with an empty track. + */ + public void testWriteTrack_emptyTrack() throws Exception { // Set expected mock behavior - formatWriter.writeHeader(); - formatWriter.writeBeginTrack(null); - formatWriter.writeEndTrack(null); - formatWriter.writeFooter(); - formatWriter.close(); + trackFormatWriter.prepare(track, outputStream); + trackFormatWriter.writeHeader(); + trackFormatWriter.writeBeginTrack(null); + trackFormatWriter.writeEndTrack(null); + trackFormatWriter.writeFooter(); + trackFormatWriter.close(); mocksControl.replay(); - writer.writeDocument(); + trackWriter.writeTrack(outputStream); - assertTrue(writer.wasSuccess()); + assertTrue(trackWriter.wasSuccess()); mocksControl.verify(); } /** - * Tests when a track only contains invalid locations. Make sure an empty - * track is written. + * Tests write track with invalid locations. Make sure an empty track is + * written. */ - public void testWriteDocument_oneInvalidLocation() throws Exception { - writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); + public void testWriteTrack_oneInvalidLocation() throws Exception { - Location[] locs = { new Location("fake0") }; - fillLocations(locs); + // Add two locations + Location[] locations = { new Location("fake0"), new Location("fake1") }; + fillLocations(locations); - // Make location invalid - locs[0].setLatitude(100); + // Make locations invalid + locations[0].setLatitude(100.0); + locations[1].setLatitude(100.0); - assertEquals(locs.length, providerUtils.bulkInsertTrackPoint(locs, locs.length, TRACK_ID)); + assertEquals(locations.length, + myTracksProviderUtils.bulkInsertTrackPoint(locations, locations.length, TRACK_ID)); - formatWriter.writeHeader(); - formatWriter.writeBeginTrack(null); - formatWriter.writeEndTrack(null); - formatWriter.writeFooter(); - formatWriter.close(); + // Set expected mock behavior + trackFormatWriter.prepare(track, outputStream); + trackFormatWriter.writeHeader(); + trackFormatWriter.writeBeginTrack(null); + trackFormatWriter.writeEndTrack(null); + trackFormatWriter.writeFooter(); + trackFormatWriter.close(); mocksControl.replay(); - writer.writeDocument(); + trackWriter.writeTrack(outputStream); - assertTrue(writer.wasSuccess()); + assertTrue(trackWriter.wasSuccess()); mocksControl.verify(); } - public void testWriteDocument() throws Exception { - writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); + /** + * Tests write track. + */ + public void testWriteTrack() throws Exception { - final Location[] locs = { - new Location("fake0"), - new Location("fake1"), - new Location("fake2"), - new Location("fake3"), - new Location("fake4"), - new Location("fake5") - }; - Waypoint[] wps = { new Waypoint(), new Waypoint(), new Waypoint() }; - - // Fill locations with valid values - fillLocations(locs); + // Add six locations + Location[] locations = { new Location("fake0"), new Location("fake1"), new Location("fake2"), + new Location("fake3"), new Location("fake4"), new Location("fake5") }; + fillLocations(locations); // Make location 3 invalid - locs[2].setLatitude(100); + locations[2].setLatitude(100.0); - assertEquals(locs.length, providerUtils.bulkInsertTrackPoint(locs, locs.length, TRACK_ID)); - for (int i = 0; i < wps.length; ++i) { - Waypoint wpt = wps[i]; - wpt.setTrackId(TRACK_ID); - assertNotNull(providerUtils.insertWaypoint(wpt)); - wpt.setId(i + 1); + assertEquals(locations.length, + myTracksProviderUtils.bulkInsertTrackPoint(locations, locations.length, TRACK_ID)); + + Waypoint[] waypoints = { new Waypoint(), new Waypoint(), new Waypoint() }; + + for (int i = 0; i < waypoints.length; i++) { + Waypoint waypoint = waypoints[i]; + waypoint.setTrackId(TRACK_ID); + assertNotNull(myTracksProviderUtils.insertWaypoint(waypoint)); + waypoint.setId(i + 1); } - formatWriter.writeHeader(); + trackFormatWriter.prepare(track, outputStream); + trackFormatWriter.writeHeader(); // Expect reading/writing of the waypoints (except the first) - formatWriter.writeBeginWaypoints(); - formatWriter.writeWaypoint(wptEq(wps[1])); - formatWriter.writeWaypoint(wptEq(wps[2])); - formatWriter.writeEndWaypoints(); + trackFormatWriter.writeBeginWaypoints(); + trackFormatWriter.writeWaypoint(waypointEq(waypoints[1])); + trackFormatWriter.writeWaypoint(waypointEq(waypoints[2])); + trackFormatWriter.writeEndWaypoints(); // Begin the track - formatWriter.writeBeginTrack(locEq(locs[0])); + trackFormatWriter.writeBeginTrack(locationEq(locations[0])); // Write locations 1-2 - formatWriter.writeOpenSegment(); - formatWriter.writeLocation(locEq(locs[0])); - formatWriter.writeLocation(locEq(locs[1])); - formatWriter.writeCloseSegment(); + trackFormatWriter.writeOpenSegment(); + trackFormatWriter.writeLocation(locationEq(locations[0])); + trackFormatWriter.writeLocation(locationEq(locations[1])); + trackFormatWriter.writeCloseSegment(); // Location 3 is not written - it's invalid // Write locations 4-6 - formatWriter.writeOpenSegment(); - formatWriter.writeLocation(locEq(locs[3])); - formatWriter.writeLocation(locEq(locs[4])); - formatWriter.writeLocation(locEq(locs[5])); - formatWriter.writeCloseSegment(); + trackFormatWriter.writeOpenSegment(); + trackFormatWriter.writeLocation(locationEq(locations[3])); + trackFormatWriter.writeLocation(locationEq(locations[4])); + trackFormatWriter.writeLocation(locationEq(locations[5])); + trackFormatWriter.writeCloseSegment(); // End the track - formatWriter.writeEndTrack(locEq(locs[5])); + trackFormatWriter.writeEndTrack(locationEq(locations[5])); - formatWriter.writeFooter(); - formatWriter.close(); + trackFormatWriter.writeFooter(); + trackFormatWriter.close(); mocksControl.replay(); - writer.writeDocument(); + trackWriter.writeTrack(outputStream); - assertTrue(writer.wasSuccess()); + assertTrue(trackWriter.wasSuccess()); mocksControl.verify(); } - private static Waypoint wptEq(final Waypoint wpt) { + /** + * Waypoint equals. + * + * @param waypoint the waypoint + */ + private Waypoint waypointEq(final Waypoint waypoint) { EasyMock.reportMatcher(new IArgumentMatcher() { - @Override - public boolean matches(Object wptObj2) { - if (wptObj2 == null || wpt == null) return wpt == wptObj2; - Waypoint wpt2 = (Waypoint) wptObj2; + @Override + public boolean matches(Object object) { + if (object == null || waypoint == null) { + return waypoint == object; + } + Waypoint waypoint2 = (Waypoint) object; - return wpt.getId() == wpt2.getId(); + return waypoint.getId() == waypoint2.getId(); } - @Override + @Override public void appendTo(StringBuffer buffer) { - buffer.append("wptEq("); - buffer.append(wpt); + buffer.append("waypointEq("); + buffer.append(waypoint); buffer.append(")"); } }); return null; } - private static Location locEq(final Location loc) { + /** + * Location equals. + * + * @param location the location + */ + private Location locationEq(final Location location) { EasyMock.reportMatcher(new IArgumentMatcher() { - @Override - public boolean matches(Object locObj2) { - if (locObj2 == null || loc == null) return loc == locObj2; - Location loc2 = (Location) locObj2; + @Override + public boolean matches(Object object) { + if (object == null || location == null) { + return location == object; + } + Location location2 = (Location) object; - return loc.hasAccuracy() == loc2.hasAccuracy() - && (!loc.hasAccuracy() || loc.getAccuracy() == loc2.getAccuracy()) - && loc.hasAltitude() == loc2.hasAltitude() - && (!loc.hasAltitude() || loc.getAltitude() == loc2.getAltitude()) - && loc.hasBearing() == loc2.hasBearing() - && (!loc.hasBearing() || loc.getBearing() == loc2.getBearing()) - && loc.hasSpeed() == loc2.hasSpeed() - && (!loc.hasSpeed() || loc.getSpeed() == loc2.getSpeed()) - && loc.getLatitude() == loc2.getLatitude() - && loc.getLongitude() == loc2.getLongitude() - && loc.getTime() == loc2.getTime(); + return location.hasAccuracy() == location2.hasAccuracy() + && (!location.hasAccuracy() || location.getAccuracy() == location2.getAccuracy()) + && location.hasAltitude() == location2.hasAltitude() + && (!location.hasAltitude() || location.getAltitude() == location2.getAltitude()) + && location.hasBearing() == location2.hasBearing() + && (!location.hasBearing() || location.getBearing() == location2.getBearing()) + && location.hasSpeed() == location2.hasSpeed() + && (!location.hasSpeed() || location.getSpeed() == location2.getSpeed()) + && location.getLatitude() == location2.getLatitude() + && location.getLongitude() == location2.getLongitude() + && location.getTime() == location2.getTime(); } - @Override + @Override public void appendTo(StringBuffer buffer) { - buffer.append("locEq("); - buffer.append(loc); + buffer.append("locationEq("); + buffer.append(location); buffer.append(")"); } }); return null; } - private void fillLocations(Location... locs) { - assertTrue(locs.length < 90); - for (int i = 0; i < locs.length; i++) { - Location location = locs[i]; + /** + * Fills the locations. + * + * @param locations the locations + */ + private void fillLocations(Location... locations) { + assertTrue(locations.length < 90); + for (int i = 0; i < locations.length; i++) { + Location location = locations[i]; location.setLatitude(i + 1); location.setLongitude(i + 1); location.setTime(i + 1000); diff --git a/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/DeleteTest.java b/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/DeleteTest.java index 9c111c7d6..5993db73d 100644 --- a/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/DeleteTest.java +++ b/MyTracksTest_E2E/src/com/google/android/apps/mytracks/endtoendtest/DeleteTest.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks.endtoendtest; import com.google.android.apps.mytracks.TrackListActivity; +import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.maps.mytracks.R; import android.app.Instrumentation; @@ -73,8 +74,10 @@ public class DeleteTest extends ActivityInstrumentationTestCase2