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
+15 -7
View File
@@ -15,13 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<plurals name="importFiles"> <plurals name="files">
<item quantity="zero">@string/import_files_zero</item> <item quantity="zero">@string/files_zero</item>
<item quantity="one">@string/import_files_one</item> <item quantity="one">@string/files_one</item>
<item quantity="two">@string/import_files_two</item> <item quantity="two">@string/files_two</item>
<item quantity="few">@string/import_files_few</item> <item quantity="few">@string/files_few</item>
<item quantity="many">@string/import_files_many</item> <item quantity="many">@string/files_many</item>
<item quantity="other">@string/import_files_other</item> <item quantity="other">@string/files_other</item>
</plurals>
<plurals name="tracks">
<item quantity="zero">@string/tracks_zero</item>
<item quantity="one">@string/tracks_one</item>
<item quantity="two">@string/tracks_two</item>
<item quantity="few">@string/tracks_few</item>
<item quantity="many">@string/tracks_many</item>
<item quantity="other">@string/tracks_other</item>
</plurals> </plurals>
<plurals name="voiceHours"> <plurals name="voiceHours">
<item quantity="zero">@string/voice_hours_zero</item> <item quantity="zero">@string/voice_hours_zero</item>
+210 -148
View File
@@ -16,7 +16,7 @@ limitations under the License.
--> -->
<resources> <resources>
<!-- About Page --> <!-- About -->
<string name="about_copyright_notice" <string name="about_copyright_notice"
translation_description="In the 'About My Tracks' page, the copyright notice."> translation_description="In the 'About My Tracks' page, the copyright notice.">
Copyright 2009 Google Inc. Copyright 2009 Google Inc.
@@ -556,7 +556,7 @@ limitations under the License.
Segment Segment
</string> </string>
<!-- EULA --> <!-- Eula -->
<string name="eula_accept" <string name="eula_accept"
translation_description="In the 'Terms of Service' dialog, the button to accept."> translation_description="In the 'Terms of Service' dialog, the button to accept.">
Accept Accept
@@ -599,44 +599,60 @@ limitations under the License.
</string> </string>
<!-- External Storage --> <!-- External Storage -->
<string name="external_storage_canceled"
translation_description="When importing/saving to the external storage, the message indicating
that the operation was canceled.">
The operation was canceled.
</string>
<string name="external_storage_not_available" <string name="external_storage_not_available"
translation_description="Error message when the external storage is not available."> translation_description="Error message when the external storage is not available.">
External storage not available. External storage not available.
</string> </string>
<string name="external_storage_not_writeable" <string name="external_storage_not_writeable"
translation_description="Error messazge when the external storage is not writeable."> translation_description="Error message when the external storage is not writeable.">
External storage not writeable. External storage not writeable.
</string> </string>
<string name="external_storage_save_error"
translation_description="When saving to the external storage, the error message indicating not <!-- Files -->
able to save."> <string name="files_few"
Unable to save to external storage. translation_description="The phrase for 'few' files. This is used when the language requires
special treatment of 'small' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a small number.">
%1$d files
</string> </string>
<string name="external_storage_save_error_create_dir" <string name="files_many"
translation_description="When saving to the external storage, the error message indicating not translation_description="The phrase for 'many' files. This is used when the language requires
able to create a directory."> special treatment of 'large' numbers. See
Unable to create a directory on the external storage. http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a large number.">
%1$d files
</string> </string>
<string name="external_storage_save_error_no_track" <string name="files_one"
translation_description="When saving to the external storage, the error message indicating that translation_description="The phrase for 1 file. This is used when the language requires special
no track is saved."> treatment of numbers like one. See
No track is saved to the external storage. http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
1 file
</string> </string>
<string name="external_storage_save_progress_message" <string name="files_other"
translation_description="When saving to the external storage, the progress message."> translation_description="The phrase for 'other' files. This is used when the language does not
Saving to external storage&#8230; require special treatment of the given quantity (as with all numbers in Chinese). See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number.">
%1$d files
</string> </string>
<string name="external_storage_save_success" <string name="files_two"
translation_description="When saving to the external storage, the success message"> translation_description="The phrase for 2 files. This is used when the language requires special
Finished saving to external storage. treatment of numbers like two. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
2 files
</string>
<string name="files_zero"
translation_description="The phrase for 0 files. This is used when the language requires special
treatment of the number 0. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
0 files
</string> </string>
<!-- Generic Strings --> <!-- Generic -->
<string name="generic_add" <string name="generic_add"
translation_description="The text on a button to say add."> translation_description="The text on a button to say add.">
Add Add
@@ -703,7 +719,7 @@ limitations under the License.
Success Success
</string> </string>
<!-- GPS --> <!-- Gps -->
<string name="gps_google_location_settings" <string name="gps_google_location_settings"
translation_description="In Android, the Google location settings to modify location settings translation_description="In Android, the Google location settings to modify location settings
for all Google applications. See Settings -> Google -> Location settings."> for all Google applications. See Settings -> Google -> Location settings.">
@@ -744,7 +760,7 @@ limitations under the License.
Waiting for GPS signal. Waiting for GPS signal.
</string> </string>
<!-- Help page --> <!-- Help -->
<string name="help_about" <string name="help_about"
translation_description="In the 'Help' page, the button on the bottom to open the 'About My translation_description="In the 'Help' page, the button on the bottom to open the 'About My
Tracks' dialog."> Tracks' dialog.">
@@ -961,47 +977,14 @@ limitations under the License.
</string> </string>
<!-- Import --> <!-- Import -->
<string name="import_files_few" <string name="import_error"
translation_description="When importing files, the phrase for 'few' files. This is used when the translation_description="When importing from the external storage, the error message indicating
language requires special treatment of 'small' numbers. See that only some of the files are imported.
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a small number."> %1$d the number of files successfully imported. E.g., '2'.
%1$d files %2$s the number of files in the directory. E.g., '2 files'.
</string> %3$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
<string name="import_files_many" Imported %1$d of %2$s from %3$s.
translation_description="When importing files, the phrase for 'many' files. This is used when
the language requires special treatment of 'large' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a large number.">
%1$d files
</string>
<string name="import_files_one"
translation_description="When importing files, the phrase for 1 file. This is used when the
language requires special treatment of numbers like one. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
1 file
</string>
<string name="import_files_other"
translation_description="When importing files, the phrase for 'other' files. This is used when
the language does not require special treatment of the given quantity (as with all numbers in
Chinese). See http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number.">
%1$d files
</string>
<string name="import_files_two"
translation_description="When importing files, the phrase for 2 files. This is used when the
language requires special treatment of numbers like two. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
2 files
</string>
<string name="import_files_zero"
translation_description="When importing files, the phrase for 0 files. This is used when the
language requires special treatment of the number 0. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
0 files
</string> </string>
<string name="import_no_directory" <string name="import_no_directory"
translation_description="When importing from the external storage, the error message when the translation_description="When importing from the external storage, the error message when the
@@ -1010,13 +993,6 @@ limitations under the License.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'."> %1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
%1$s does not exist. %1$s does not exist.
</string> </string>
<string name="import_no_file"
translation_description="When importing from the external storage, the message indicating
that no file is found.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
No file found in %1$s.
</string>
<string name="import_progress_message" <string name="import_progress_message"
translation_description="When importing from the external storage, the progress message. translation_description="When importing from the external storage, the progress message.
@@ -1026,10 +1002,9 @@ limitations under the License.
<string name="import_success" <string name="import_success"
translation_description="When importing from the external storage, the success message. translation_description="When importing from the external storage, the success message.
%1$d the number of files successfully imported. E.g., '2'. %1$s the number of files. E.g., '2 files'.
%2$s the number of files in the directory. E.g., '2 files'. %2$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
%3$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'."> Imported %1$s from %2$s.
Imported %1$d of %2$s from %3$s.
</string> </string>
<!-- Maps List --> <!-- Maps List -->
@@ -1079,27 +1054,6 @@ limitations under the License.
user that the marker will be permanently deleted from the device."> user that the marker will be permanently deleted from the device.">
The marker will be permanently deleted from the device. The marker will be permanently deleted from the device.
</string> </string>
<string name="marker_detail_statistics_title"
translation_description="In the marker detail page, the title for the statistics section
indicating that the data below are since the last split marker.">
Since last split marker:
</string>
<string name="marker_detail_waypoint_marker_type"
translation_description="In the marker detail page, the label for the marker type.
%1$s marker type. E.g., 'building'.">
Marker type: %1$s
</string>
<string name="marker_edit_marker_type"
translation_description="In the marker edit page, the label for the marker type, including an
example.">
Marker type (e.g. waterfall)
</string>
<string name="marker_edit_marker_type_hint"
translation_description="In the marker edit page, the hint in the marker type text field when it
is empty.">
Marker type
</string>
<string name="marker_label_end" <string name="marker_label_end"
translation_description="When inserting a marker to indicate the end of a track, the marker's translation_description="When inserting a marker to indicate the end of a track, the marker's
name. E.g., 'track_name (End)'. name. E.g., 'track_name (End)'.
@@ -1114,10 +1068,6 @@ limitations under the License.
%1$s track name. E.g., 'track_name'."> %1$s track name. E.g., 'track_name'.">
%1$s (Start) %1$s (Start)
</string> </string>
<string name="marker_list_empty_message"
translation_description="The message when the marker list is empty.">
You don\'t have any markers.
</string>
<string name="marker_name_format" <string name="marker_name_format"
translation_description="When adding a marker, the string format for the default name. translation_description="When adding a marker, the string format for the default name.
@@ -1131,7 +1081,38 @@ limitations under the License.
Split %1$d Split %1$d
</string> </string>
<!-- Menu Items --> <!-- Marker Detail -->
<string name="marker_detail_statistics_title"
translation_description="In the marker detail page, the title for the statistics section
indicating that the data below are since the last split marker.">
Since last split marker:
</string>
<string name="marker_detail_waypoint_marker_type"
translation_description="In the marker detail page, the label for the marker type.
%1$s marker type. E.g., 'building'.">
Marker type: %1$s
</string>
<!-- Marker Edit -->
<string name="marker_edit_marker_type"
translation_description="In the marker edit page, the label for the marker type, including an
example.">
Marker type (e.g. waterfall)
</string>
<string name="marker_edit_marker_type_hint"
translation_description="In the marker edit page, the hint in the marker type text field when it
is empty.">
Marker type
</string>
<!-- Marker List -->
<string name="marker_list_empty_message"
translation_description="The message when the marker list is empty.">
You don\'t have any markers.
</string>
<!-- Menu -->
<string name="menu_aggregated_statistics" <string name="menu_aggregated_statistics"
translation_description="In the menu, to go to the aggregated statistics page. translation_description="In the menu, to go to the aggregated statistics page.
@@ -1373,14 +1354,28 @@ limitations under the License.
write data to Google My Tracks and start/stop Google My Tracks recording write data to Google My Tracks and start/stop Google My Tracks recording
</string> </string>
<!-- Restore --> <!-- Save -->
<string name="restore_error" <string name="save_error"
translation_description="When restoring from backup, the error message."> translation_description="When saving to the external storage, the error message indicating that
Unable to restore from backup. only some of the tracks are saved.
%1$d the number of tracks successfully saved. E.g., '1'.
%1$s the number of tracks to save. E.g., '2 tracks'.
%2$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Saved %1$d of %2$s to %3$s.
</string> </string>
<string name="restore_success" <string name="save_progress_message"
translation_description="When restoring from backup, the success message."> translation_description="When saving to the external storage, the progress message.
Finished restoring from backup.
%1$s: the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Saving to %1$s&#8230;
</string>
<string name="save_success"
translation_description="When saving to the external storage, the success message.
%1$s the number of tracks. E.g., '2 tracks'.
%2$s the directory. E.g., '/storage/sdcard0/MyTracks/gpx/'.">
Saved %1$s to %2$s.
</string> </string>
<!-- Search --> <!-- Search -->
@@ -1586,6 +1581,25 @@ limitations under the License.
Sending Sending
</string> </string>
<!-- Settings -->
<string name="settings_not_while_recording"
translation_description="In settings, the summary for an option that is not available during a
track recording.">
Not available while recording a track.
</string>
<string name="settings_show"
translation_description="In settings, the section label to show additional values.
Character limit: ~26 (1 line)">
Show
</string>
<string name="settings_switch_account"
translation_description="In the 'Settings' page, the option to switch account.
Character limit: ~26 (1 line)">
Switch account
</string>
<!-- Settings Backup --> <!-- Settings Backup -->
<string name="settings_backup" <string name="settings_backup"
translation_description="In the 'Settings' page, the option to change the data backup related translation_description="In the 'Settings' page, the option to change the data backup related
@@ -1594,6 +1608,8 @@ limitations under the License.
Character limit: ~26 (1 line)"> Character limit: ~26 (1 line)">
Backup Backup
</string> </string>
<!-- Settings Backup Now -->
<string name="settings_backup_now" <string name="settings_backup_now"
translation_description="In the 'Backup' settings, the option to backup to the external storage translation_description="In the 'Backup' settings, the option to backup to the external storage
now. now.
@@ -1601,6 +1617,10 @@ limitations under the License.
Character limit: ~26 (1 line)"> Character limit: ~26 (1 line)">
Backup now Backup now
</string> </string>
<string name="settings_backup_now_error"
translation_description="When backing up to the external storage, the error messag.">
Unable to backup.
</string>
<string name="settings_backup_now_progress_message" <string name="settings_backup_now_progress_message"
translation_description="In the 'Backup' settings, after pressing the 'Backup now' option, the translation_description="In the 'Backup' settings, after pressing the 'Backup now' option, the
progress message telling the user that it is writing all tracks and settings to the external progress message telling the user that it is writing all tracks and settings to the external
@@ -1612,6 +1632,12 @@ limitations under the License.
explaining that all tracks and settings will be written to the external storage."> explaining that all tracks and settings will be written to the external storage.">
Write a full copy of all your tracks and settings to the external storage. Write a full copy of all your tracks and settings to the external storage.
</string> </string>
<string name="settings_backup_now_success"
translation_description="When backing up to the external storage, the success message">
Finished backup.
</string>
<!-- Settings Backup Restore -->
<string name="settings_backup_restore" <string name="settings_backup_restore"
translation_description="In the 'Backup' settings, the option to restore from the external translation_description="In the 'Backup' settings, the option to restore from the external
storage. storage.
@@ -1626,6 +1652,10 @@ limitations under the License.
Restoring a backup from the external storage will overwrite all your current tracks and Restoring a backup from the external storage will overwrite all your current tracks and
settings. settings.
</string> </string>
<string name="settings_backup_restore_error"
translation_description="When restoring from backup, the error message.">
Unable to restore from backup.
</string>
<string name="settings_backup_restore_no_backup" <string name="settings_backup_restore_no_backup"
translation_description="In the 'Backup' settings, after pressing the 'Restore now' option, the translation_description="In the 'Backup' settings, after pressing the 'Restore now' option, the
message in the alert dialog informing the user that there is no backup to restore from."> message in the alert dialog informing the user that there is no backup to restore from.">
@@ -1647,6 +1677,10 @@ limitations under the License.
explaining that it restores all tracks and settings from the external storage."> explaining that it restores all tracks and settings from the external storage.">
Restore all tracks and settings from the external storage. Restore all tracks and settings from the external storage.
</string> </string>
<string name="settings_backup_restore_success"
translation_description="When restoring from backup, the success message.">
Finished restoring from backup.
</string>
<!-- Settings Chart --> <!-- Settings Chart -->
<string name="settings_chart_by_distance" <string name="settings_chart_by_distance"
@@ -1731,13 +1765,6 @@ limitations under the License.
The way to color a track based on slow, medium, and fast speed. The way to color a track based on slow, medium, and fast speed.
</string> </string>
<!-- Settings Not While Recording -->
<string name="settings_not_while_recording"
translation_description="In settings, the summary for an option that is not available during a
track recording.">
Not available while recording a track.
</string>
<!-- Settings Recording --> <!-- Settings Recording -->
<string name="settings_recording" <string name="settings_recording"
translation_description="In the 'Settings' page, the option to change the track recording translation_description="In the 'Settings' page, the option to change the track recording
@@ -2093,14 +2120,6 @@ limitations under the License.
New maps created in Google Maps will be public, instead of unlisted. See %1$s for more info. New maps created in Google Maps will be public, instead of unlisted. See %1$s for more info.
</string> </string>
<!-- Settings Show -->
<string name="settings_show"
translation_description="In settings, the section label to show additional values.
Character limit: ~26 (1 line)">
Show
</string>
<!-- Settings Stats --> <!-- Settings Stats -->
<string name="settings_stats_coordinate" <string name="settings_stats_coordinate"
translation_description="In the 'Stats settings, the option to show latitude and longitude. translation_description="In the 'Stats settings, the option to show latitude and longitude.
@@ -2140,14 +2159,6 @@ limitations under the License.
Preferred units Preferred units
</string> </string>
<!-- Settings Switch Account -->
<string name="settings_switch_account"
translation_description="In the 'Settings' page, the option to switch account.
Character limit: ~26 (1 line)">
Switch account
</string>
<!-- Share Track --> <!-- Share Track -->
<string name="share_track_confirm_message" <string name="share_track_confirm_message"
translation_description="The confirmation message when sharing a track with friends. translation_description="The confirmation message when sharing a track with friends.
@@ -2193,7 +2204,7 @@ limitations under the License.
I would like to share a track with you I would like to share a track with you
</string> </string>
<!-- Stats Page --> <!-- Stats -->
<string name="stats_average_moving_pace" <string name="stats_average_moving_pace"
translation_description="In the 'Stats' page, the average moving pace."> translation_description="In the 'Stats' page, the average moving pace.">
Avg moving pace Avg moving pace
@@ -2278,6 +2289,19 @@ limitations under the License.
</string> </string>
<!-- Track --> <!-- Track -->
<string name="track_name_format"
translation_description="When creating a new track, the string format to name the track.
%1$d: track number. E.g., '1'.">
Track %1$d
</string>
<string name="track_record_notification"
translation_description="When recording a track, the message on the My Tracks notification
informing the user that My Tracks is actively recording his track.">
Recording your track&#8230;
</string>
<!-- Track Detail -->
<string name="track_detail_chart_tab" <string name="track_detail_chart_tab"
translation_description="In the track detail page, the title of the tab showing the chart."> translation_description="In the track detail page, the title of the tab showing the chart.">
Chart Chart
@@ -2310,6 +2334,8 @@ limitations under the License.
translation_description="In the track detail page, the title of the tab showing the stats."> translation_description="In the track detail page, the title of the tab showing the stats.">
Stats Stats
</string> </string>
<!-- Track Edit -->
<string name="track_edit_activity_type" <string name="track_edit_activity_type"
translation_description="In the page to edit a track, the label for the text field to edit the translation_description="In the page to edit a track, the label for the text field to edit the
activity type, including an example."> activity type, including an example.">
@@ -2324,6 +2350,8 @@ limitations under the License.
translation_description="The title of the track edit page when saving a new track."> translation_description="The title of the track edit page when saving a new track.">
New Track New Track
</string> </string>
<!-- Track List -->
<string name="track_list_delete_all_confirm_message" <string name="track_list_delete_all_confirm_message"
translation_description="When deleting all the tracks, the confirmation dialog message informing translation_description="When deleting all the tracks, the confirmation dialog message informing
the user that all tracks and markers will be permanently deleted from the device."> the user that all tracks and markers will be permanently deleted from the device.">
@@ -2344,17 +2372,6 @@ limitations under the License.
success."> success.">
Now recording a new track. Now recording a new track.
</string> </string>
<string name="track_name_format"
translation_description="When creating a new track, the string format to name the track.
%1$d: track number. E.g., '1'.">
Track %1$d
</string>
<string name="track_record_notification"
translation_description="When recording a track, the message on the My Tracks notification
informing the user that My Tracks is actively recording his track.">
Recording your track&#8230;
</string>
<!-- Track Widget --> <!-- Track Widget -->
<string name="track_widget_config_title" <string name="track_widget_config_title"
@@ -2362,6 +2379,50 @@ limitations under the License.
Configure Widget Items Configure Widget Items
</string> </string>
<!-- Tracks -->
<string name="tracks_few"
translation_description="The phrase for 'few' tracks. This is used when the language requires
special treatment of 'small' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a small number.">
%1$d tracks
</string>
<string name="tracks_many"
translation_description="The phrase for 'many' tracks. This is used when the language requires
special treatment of 'large' numbers. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number. The number is a large number.">
%1$d tracks
</string>
<string name="tracks_one"
translation_description="The phrase for 1 track. This is used when the language requires special
treatment of numbers like one. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
1 track
</string>
<string name="tracks_other"
translation_description="The phrase for 'other' tracks. This is used when the language does not
require special treatment of the given quantity (as with all numbers in Chinese). See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals
%1$d an integer number.">
%1$d tracks
</string>
<string name="tracks_two"
translation_description="The phrase for 2 tracks. This is used when the language requires
special treatment of numbers like two. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
2 tracks
</string>
<string name="tracks_zero"
translation_description="The phrase for 0 tracks. This is used when the language requires
special treatment of the number 0. See
http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
0 tracks
</string>
<!-- Unit --> <!-- Unit -->
<string name="unit_feet" <string name="unit_feet"
translation_description="Abbreviation for feet. E.g., '2 ft'."> translation_description="Abbreviation for feet. E.g., '2 ft'.">
@@ -2395,6 +2456,7 @@ limitations under the License.
translation_description="Abbreviation for minutes per mile. E.g, '5 min/mi'."> translation_description="Abbreviation for minutes per mile. E.g, '5 min/mi'.">
min/mi min/mi
</string> </string>
<!-- Value --> <!-- Value -->
<string name="value_adapt_accuracy" <string name="value_adapt_accuracy"
translation_description="In a setting picker, the value to adapt for accuracy."> translation_description="In a setting picker, the value to adapt for accuracy.">
@@ -56,7 +56,7 @@ public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.externalFileBackup = new ExternalFileBackup(backupActivity); this.externalFileBackup = new ExternalFileBackup(backupActivity);
success = false; success = false;
completed = 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)) { if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
messageId = R.string.external_storage_save_error_create_dir; messageId = R.string.external_storage_not_writeable;
return false; return false;
} }
try { try {
externalFileBackup.writeToDefaultFile(); externalFileBackup.writeToDefaultFile();
messageId = R.string.external_storage_save_success; messageId = R.string.settings_backup_now_success;
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "IO exception", e); Log.d(TAG, "IO exception", e);
@@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.externalFileBackup = new ExternalFileBackup(restoreActivity); this.externalFileBackup = new ExternalFileBackup(restoreActivity);
success = false; success = false;
completed = 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) { protected Boolean doInBackground(Void... params) {
try { try {
externalFileBackup.restoreFromDate(date); externalFileBackup.restoreFromDate(date);
messageId = R.string.restore_success; messageId = R.string.settings_backup_restore_success;
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "IO exception", e); Log.d(TAG, "IO exception", e);
@@ -61,7 +61,7 @@ public class ImportActivity extends Activity {
// the path on the external storage to import // the path on the external storage to import
private String path; private String path;
// the number of successfully imported files // the number of files successfully imported
private int successCount; private int successCount;
// the number of files to import // the number of files to import
@@ -137,29 +137,35 @@ public class ImportActivity extends Activity {
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true); importAsyncTask.cancel(true);
dialog.dismiss();
finish(); finish();
} }
}, path); }, path);
return progressDialog; return progressDialog;
case DIALOG_RESULT_ID: case DIALOG_RESULT_ID:
final boolean success;
String message; String message;
if (totalCount == 0) { String totalFiles = getResources()
message = getString(R.string.import_no_file, path); .getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount && totalCount > 0) {
success = true;
message = getString(R.string.import_success, totalFiles, path);
} else { } else {
String totalFiles = getResources() success = false;
.getQuantityString(R.plurals.importFiles, totalCount, totalCount); message = getString(R.string.import_error, successCount, totalFiles, path);
message = getString(R.string.import_success, 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() { .setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish(); finish();
} }
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
if (!importAll && trackId != -1L) { if (success && !importAll && trackId != -1L) {
Intent intent = IntentUtils.newIntent( Intent intent = IntentUtils.newIntent(
ImportActivity.this, TrackDetailActivity.class) ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
@@ -167,9 +173,11 @@ public class ImportActivity extends Activity {
taskStackBuilder.addNextIntent(intent); taskStackBuilder.addNextIntent(intent);
taskStackBuilder.startActivities(); taskStackBuilder.startActivities();
} }
dialog.dismiss();
finish(); finish();
} }
}).create(); }).setTitle(success ? R.string.generic_success_title : R.string.generic_error_title)
.create();
default: default:
return null; return null;
} }
@@ -179,7 +187,7 @@ public class ImportActivity extends Activity {
* Invokes when the associated AsyncTask completes. * Invokes when the associated AsyncTask completes.
* *
* @param aSuccessCount the number of files successfully imported * @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 * @param aTrackId the last successfully imported track id
*/ */
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) { 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.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
@@ -42,6 +43,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final boolean importAll; private final boolean importAll;
private final TrackFileFormat trackFileFormat; private final TrackFileFormat trackFileFormat;
private final String path; private final String path;
private final Context context;
private WakeLock wakeLock; private WakeLock wakeLock;
// true if the AsyncTask has completed // true if the AsyncTask has completed
@@ -70,6 +72,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.importAll = importAll; this.importAll = importAll;
this.trackFileFormat = trackFileFormat; this.trackFileFormat = trackFileFormat;
this.path = path; this.path = path;
context = importActivity.getApplicationContext();
// Get the wake lock if not recording or paused // Get the wake lock if not recording or paused
if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key) if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
@@ -154,9 +157,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*/ */
private boolean importFile(final File file) { private boolean importFile(final File file) {
try { try {
AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter( AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter(context)
importActivity) : new GpxImporter(context);
: new GpxImporter(importActivity);
long trackIds[] = importer.importFile(new FileInputStream(file)); long trackIds[] = importer.importFile(new FileInputStream(file));
int length = trackIds.length; int length = trackIds.length;
if (length > 0) { if (length > 0) {
@@ -17,6 +17,7 @@
package com.google.android.apps.mytracks.io.file; package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.util.DialogUtils; 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.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -28,12 +29,13 @@ import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import java.io.File; import java.io.File;
/** /**
* An activity for saving tracks to the SD card. If saving a specific track, * An activity for saving tracks to the external storage. If saving a specific
* option to save it to a temp directory and play the track afterward. * track, option to save it to a temp directory and play the track afterward.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -57,17 +59,18 @@ public class SaveActivity extends Activity {
private TrackFileFormat trackFileFormat; private TrackFileFormat trackFileFormat;
private long trackId; private long trackId;
private boolean playTrack; private boolean playTrack;
private String directoryName;
private SaveAsyncTask saveAsyncTask; private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
// result from the AsyncTask // the number of tracks successfully saved
private boolean success; private int successCount;
// message id from the AsyncTask // the number of tracks to save
private int messageId; private int totalCount;
// saved file path from the AsyncTask // the last successfully saved path
private String savedPath; private String savedPath;
@Override @Override
@@ -79,12 +82,29 @@ public class SaveActivity extends Activity {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L); trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false); 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(); Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) { if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained; saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this); saveAsyncTask.setActivity(this);
} else { } else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackId, playTrack); saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackId, directory);
saveAsyncTask.execute(); saveAsyncTask.execute();
} }
} }
@@ -99,37 +119,43 @@ public class SaveActivity extends Activity {
protected Dialog onCreateDialog(int id) { protected Dialog onCreateDialog(int id) {
switch (id) { switch (id) {
case DIALOG_PROGRESS_ID: case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this, progressDialog = DialogUtils.createHorizontalProgressDialog(
R.string.external_storage_save_progress_message, this, R.string.save_progress_message, new DialogInterface.OnCancelListener() {
new DialogInterface.OnCancelListener() {
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true); saveAsyncTask.cancel(true);
dialog.dismiss();
finish(); finish();
} }
}); }, directoryName);
return progressDialog; return progressDialog;
case DIALOG_RESULT_ID: case DIALOG_RESULT_ID:
AlertDialog.Builder builder = new AlertDialog.Builder(this) boolean success;
.setCancelable(true) String message;
.setIcon(success String totalTracks = getResources()
? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert) .getQuantityString(R.plurals.tracks, totalCount, totalCount);
.setMessage(messageId) if (successCount == totalCount && totalCount > 0) {
.setOnCancelListener(new DialogInterface.OnCancelListener() { success = true;
@Override 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) { public void onCancel(DialogInterface dialog) {
dialog.dismiss(); dialog.dismiss();
finish(); finish();
} }
}) }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { @Override
@Override
public void onClick(DialogInterface dialog, int arg1) { public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss(); dialog.dismiss();
finish(); 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) { if (success && trackId != -1L && !playTrack && savedPath != null) {
builder.setNegativeButton( builder.setNegativeButton(
@@ -153,18 +179,18 @@ public class SaveActivity extends Activity {
/** /**
* Invokes when the associated AsyncTask completes. * Invokes when the associated AsyncTask completes.
* *
* @param isSuccess true if the AsyncTask is successful * @param aSuccessCount the number of tracks successfully saved
* @param aMessageId the id of the AsyncTask message * @param aTotalCount the number of tracks to save
* @param aSavedPath the path of the saved file * @param aSavedPath the last successfully saved path
*/ */
public void onAsyncTaskCompleted(boolean isSuccess, int aMessageId, String aSavedPath) { public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
this.success = isSuccess; successCount = aSuccessCount;
this.messageId = aMessageId; totalCount = aTotalCount;
this.savedPath = aSavedPath; savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
if (success && playTrack && savedPath != null) { if (successCount == 1 && totalCount == 1 && playTrack && savedPath != null) {
Intent intent = new Intent() Intent intent = new Intent().addFlags(
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(GOOGLE_EARTH_TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID) .putExtra(GOOGLE_EARTH_TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID)
.setClassName(GOOGLE_EARTH_PACKAGE, GOOGLE_EARTH_CLASS) .setClassName(GOOGLE_EARTH_PACKAGE, GOOGLE_EARTH_CLASS)
.setDataAndType(Uri.fromFile(new File(savedPath)), GOOGLE_EARTH_KML_MIME_TYPE); .setDataAndType(Uri.fromFile(new File(savedPath)), GOOGLE_EARTH_KML_MIME_TYPE);
@@ -17,6 +17,7 @@
package com.google.android.apps.mytracks.io.file; package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; 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.content.TracksColumns;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -30,9 +31,12 @@ import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
import java.io.File; 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 * @author Jimmy Shih
*/ */
@@ -43,42 +47,41 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private SaveActivity saveActivity; private SaveActivity saveActivity;
private final TrackFileFormat trackFileFormat; private final TrackFileFormat trackFileFormat;
private final long trackId; private final long trackId;
private final boolean useTempDir; private final File directory;
private final Context context; private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils; private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock; private WakeLock wakeLock;
private TrackWriter trackWriter; private TrackWriter trackWriter;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed // true if the AsyncTask has completed
private boolean completed; private boolean completed;
// message id to return to the activity // the number of tracks successfully saved
private int messageId; 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; private String savedPath;
/** /**
* Creates an AsyncTask. * Creates an AsyncTask.
* *
* @param saveActivity the activity currently associated with this task. * @param saveActivity the activity currently associated with this task
* @param trackFileFormat the track format * @track id the track id to save, -1L to save all tracks
* @param trackId the track id * @param trackFileFormat the track file format
* @param useTempDir true to use the temp directory * @param directory the directory to save to
*/ */
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long trackId, public SaveAsyncTask(
boolean useTempDir) { SaveActivity saveActivity, TrackFileFormat trackFileFormat, long trackId, File directory) {
this.saveActivity = saveActivity; this.saveActivity = saveActivity;
this.trackFileFormat = trackFileFormat; this.trackFileFormat = trackFileFormat;
this.trackId = trackId; this.trackId = trackId;
this.useTempDir = useTempDir; this.directory = directory;
context = saveActivity.getApplicationContext(); context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(saveActivity);
// Get the wake lock if not recording or paused // Get the wake lock if not recording or paused
if (PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key) if (PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
@@ -87,10 +90,9 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock); wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
} }
trackWriter = null;
success = false;
completed = false; completed = false;
messageId = R.string.external_storage_save_error; successCount = 0;
totalCount = 0;
savedPath = null; savedPath = null;
} }
@@ -102,7 +104,7 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
public void setActivity(SaveActivity saveActivity) { public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity; this.saveActivity = saveActivity;
if (completed && saveActivity != null) { 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) { protected Boolean doInBackground(Void... params) {
try { try {
if (trackId != -1L) { if (trackId != -1L) {
return saveOneTrack(trackId, true); totalCount = 1;
if (saveOneTrack(trackId)) {
successCount = 1;
return true;
} else {
return false;
}
} else { } else {
return saveAllTracks(); 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. * Saves one track.
* *
* @param id the track id * @param id the track id
* @param updateSavingProgress true to update the saving progress
*/ */
private Boolean saveOneTrack(long id, final boolean updateSavingProgress) { private Boolean saveOneTrack(long id) {
trackWriter = TrackWriterFactory.newWriter( Track track = myTracksProviderUtils.getTrack(id);
context, myTracksProviderUtils, id, trackFileFormat); if (track == null) {
if (trackWriter == null) { Log.d(TAG, "No track for " + id);
Log.e(TAG, "Track writer is null");
return false; return false;
} }
if (useTempDir) {
String dirName = FileUtils.buildExternalDirectoryPath(trackFileFormat.getExtension(), "tmp"); // Make sure the file doesn't exist yet (possibly by changing the filename)
trackWriter.setDirectory(new File(dirName)); 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 trackWriter = new TrackWriter(
public void onWrite(int number, int max) { context, myTracksProviderUtils, track, trackFileFormat, new TrackWriter.OnWriteListener() {
// Update the progress dialog once every 500 points @Override
if (updateSavingProgress && number % 500 == 0) { public void onWrite(int number, int max) {
publishProgress(number, 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(); return trackWriter.wasSuccess();
} }
@@ -169,27 +221,21 @@ public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try { try {
cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID); cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID);
if (cursor == null) { 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; return false;
} }
totalCount = cursor.getCount();
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID); int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
for (int i = 0; i < count; i++) { for (int i = 0; i < totalCount; i++) {
if (isCancelled()) { if (isCancelled()) {
return false; return false;
} }
cursor.moveToPosition(i); cursor.moveToPosition(i);
long id = cursor.getLong(idIndex); long id = cursor.getLong(idIndex);
if (!saveOneTrack(id, false)) { if (saveOneTrack(id)) {
return false; successCount++;
} }
publishProgress(i + 1, count); publishProgress(i + 1, totalCount);
} }
messageId = R.string.external_storage_save_success;
return true; return true;
} finally { } finally {
if (cursor != null) { 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 * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.io.file; 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 * Track Writer for writing a track to an {@link OutputStream}.
* 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 Sandor Dornbush
* @author Rodrigo Damazio * @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 { 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 number the location number
* @param max the maximum number of locations, for calculation of * @param max the maximum number of locations, for calculation of completion
* completion percentage * percentage
*/ */
public void onWrite(int number, int max); public void onWrite(int number, int max);
} }
/** private final static String TAG = TrackWriter.class.getSimpleName();
* Sets a listener to be invoked for each location writer.
*/ private final MyTracksProviderUtils myTracksProviderUtils;
void setOnWriteListener(OnWriteListener onWriteListener); 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); public TrackWriter(Context context, MyTracksProviderUtils myTracksProviderUtils, Track track,
TrackFileFormat trackFileFormat, OnWriteListener onWriteListener) {
this(myTracksProviderUtils, track, trackFileFormat.newFormatWriter(context), onWriteListener);
}
/** @VisibleForTesting
* Returns the absolute path to the file which was created. public TrackWriter(MyTracksProviderUtils myTracksProviderUtils, Track track,
*/ TrackFormatWriter trackFormatWriter, OnWriteListener onWriteListener) {
String getAbsolutePath(); this.myTracksProviderUtils = myTracksProviderUtils;
this.track = track;
/** this.trackFormatWriter = trackFormatWriter;
* Writes the given track id to the SD card. this.onWriteListener = onWriteListener;
* This is blocking. }
*/
void writeTrack();
/**
* Stop any in-progress writes
*/
void stopWriteTrack();
/** /**
* Returns true if the write completed successfully. * 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();
}
}
}
@@ -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;
}
}
@@ -2,13 +2,12 @@
package com.google.android.apps.mytracks.io.file; 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.MyTracksProvider;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriter.OnWriteListener;
import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext;
import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
@@ -19,8 +18,6 @@ import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver; import android.test.mock.MockContentResolver;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream; import java.io.OutputStream;
import org.easymock.EasyMock; import org.easymock.EasyMock;
@@ -28,112 +25,24 @@ import org.easymock.IArgumentMatcher;
import org.easymock.IMocksControl; import org.easymock.IMocksControl;
/** /**
* Tests for the track writer. * Tests for {@link TrackWriter}.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackWriterTest extends AndroidTestCase { 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 long TRACK_ID = 1234567L;
private static final String EXTENSION = "ext";
private static final String TRACK_NAME = "Swimming across the pacific"; 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 MyTracksProviderUtils myTracksProviderUtils;
private TrackFormatWriter formatWriter;
private TrackWriterImpl writer;
private IMocksControl mocksControl;
private MyTracksProviderUtils providerUtils;
private Factory oldProviderUtilsFactory; private Factory oldProviderUtilsFactory;
// State used in specific tests private IMocksControl mocksControl;
private int writeDocumentCalls;
private int openFileCalls; private TrackFormatWriter trackFormatWriter;
private Track track;
private OutputStream outputStream;
private TrackWriter trackWriter;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
@@ -143,20 +52,31 @@ public class TrackWriterTest extends AndroidTestCase {
RenamingDelegatingContext targetContext = new RenamingDelegatingContext( RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test."); getContext(), getContext(), "test.");
Context context = new MockContext(mockContentResolver, targetContext); Context context = new MockContext(mockContentResolver, targetContext);
MyTracksProvider provider = new MyTracksProvider(); MyTracksProvider myTracksProvider = new MyTracksProvider();
provider.attachInfo(context, null); myTracksProvider.attachInfo(context, null);
mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, myTracksProvider);
setContext(context); setContext(context);
providerUtils = MyTracksProviderUtils.Factory.get(context); myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(
myTracksProviderUtils);
mocksControl = EasyMock.createStrictControl(); mocksControl = EasyMock.createStrictControl();
formatWriter = mocksControl.createMock(TrackFormatWriter.class); trackFormatWriter = mocksControl.createMock(TrackFormatWriter.class);
expect(formatWriter.getExtension()).andStubReturn(EXTENSION);
track = new Track(); track = new Track();
track.setName(TRACK_NAME); track.setName(TRACK_NAME);
track.setId(TRACK_ID); 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 @Override
@@ -165,226 +85,196 @@ public class TrackWriterTest extends AndroidTestCase {
super.tearDown(); super.tearDown();
} }
public void testWriteTrack() { /**
writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, * Tests write track with an empty track.
formatWriter, true); */
public void testWriteTrack_emptyTrack() throws Exception {
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);
// Set expected mock behavior // Set expected mock behavior
formatWriter.writeHeader(); trackFormatWriter.prepare(track, outputStream);
formatWriter.writeBeginTrack(null); trackFormatWriter.writeHeader();
formatWriter.writeEndTrack(null); trackFormatWriter.writeBeginTrack(null);
formatWriter.writeFooter(); trackFormatWriter.writeEndTrack(null);
formatWriter.close(); trackFormatWriter.writeFooter();
trackFormatWriter.close();
mocksControl.replay(); mocksControl.replay();
writer.writeDocument(); trackWriter.writeTrack(outputStream);
assertTrue(writer.wasSuccess()); assertTrue(trackWriter.wasSuccess());
mocksControl.verify(); mocksControl.verify();
} }
/** /**
* Tests when a track only contains invalid locations. Make sure an empty * Tests write track with invalid locations. Make sure an empty track is
* track is written. * written.
*/ */
public void testWriteDocument_oneInvalidLocation() throws Exception { public void testWriteTrack_oneInvalidLocation() throws Exception {
writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter);
Location[] locs = { new Location("fake0") }; // Add two locations
fillLocations(locs); Location[] locations = { new Location("fake0"), new Location("fake1") };
fillLocations(locations);
// Make location invalid // Make locations invalid
locs[0].setLatitude(100); 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(); // Set expected mock behavior
formatWriter.writeBeginTrack(null); trackFormatWriter.prepare(track, outputStream);
formatWriter.writeEndTrack(null); trackFormatWriter.writeHeader();
formatWriter.writeFooter(); trackFormatWriter.writeBeginTrack(null);
formatWriter.close(); trackFormatWriter.writeEndTrack(null);
trackFormatWriter.writeFooter();
trackFormatWriter.close();
mocksControl.replay(); mocksControl.replay();
writer.writeDocument(); trackWriter.writeTrack(outputStream);
assertTrue(writer.wasSuccess()); assertTrue(trackWriter.wasSuccess());
mocksControl.verify(); 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 = { // Add six locations
new Location("fake0"), Location[] locations = { new Location("fake0"), new Location("fake1"), new Location("fake2"),
new Location("fake1"), new Location("fake3"), new Location("fake4"), new Location("fake5") };
new Location("fake2"), fillLocations(locations);
new Location("fake3"),
new Location("fake4"),
new Location("fake5")
};
Waypoint[] wps = { new Waypoint(), new Waypoint(), new Waypoint() };
// Fill locations with valid values
fillLocations(locs);
// Make location 3 invalid // Make location 3 invalid
locs[2].setLatitude(100); locations[2].setLatitude(100.0);
assertEquals(locs.length, providerUtils.bulkInsertTrackPoint(locs, locs.length, TRACK_ID)); assertEquals(locations.length,
for (int i = 0; i < wps.length; ++i) { myTracksProviderUtils.bulkInsertTrackPoint(locations, locations.length, TRACK_ID));
Waypoint wpt = wps[i];
wpt.setTrackId(TRACK_ID); Waypoint[] waypoints = { new Waypoint(), new Waypoint(), new Waypoint() };
assertNotNull(providerUtils.insertWaypoint(wpt));
wpt.setId(i + 1); 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) // Expect reading/writing of the waypoints (except the first)
formatWriter.writeBeginWaypoints(); trackFormatWriter.writeBeginWaypoints();
formatWriter.writeWaypoint(wptEq(wps[1])); trackFormatWriter.writeWaypoint(waypointEq(waypoints[1]));
formatWriter.writeWaypoint(wptEq(wps[2])); trackFormatWriter.writeWaypoint(waypointEq(waypoints[2]));
formatWriter.writeEndWaypoints(); trackFormatWriter.writeEndWaypoints();
// Begin the track // Begin the track
formatWriter.writeBeginTrack(locEq(locs[0])); trackFormatWriter.writeBeginTrack(locationEq(locations[0]));
// Write locations 1-2 // Write locations 1-2
formatWriter.writeOpenSegment(); trackFormatWriter.writeOpenSegment();
formatWriter.writeLocation(locEq(locs[0])); trackFormatWriter.writeLocation(locationEq(locations[0]));
formatWriter.writeLocation(locEq(locs[1])); trackFormatWriter.writeLocation(locationEq(locations[1]));
formatWriter.writeCloseSegment(); trackFormatWriter.writeCloseSegment();
// Location 3 is not written - it's invalid // Location 3 is not written - it's invalid
// Write locations 4-6 // Write locations 4-6
formatWriter.writeOpenSegment(); trackFormatWriter.writeOpenSegment();
formatWriter.writeLocation(locEq(locs[3])); trackFormatWriter.writeLocation(locationEq(locations[3]));
formatWriter.writeLocation(locEq(locs[4])); trackFormatWriter.writeLocation(locationEq(locations[4]));
formatWriter.writeLocation(locEq(locs[5])); trackFormatWriter.writeLocation(locationEq(locations[5]));
formatWriter.writeCloseSegment(); trackFormatWriter.writeCloseSegment();
// End the track // End the track
formatWriter.writeEndTrack(locEq(locs[5])); trackFormatWriter.writeEndTrack(locationEq(locations[5]));
formatWriter.writeFooter(); trackFormatWriter.writeFooter();
formatWriter.close(); trackFormatWriter.close();
mocksControl.replay(); mocksControl.replay();
writer.writeDocument(); trackWriter.writeTrack(outputStream);
assertTrue(writer.wasSuccess()); assertTrue(trackWriter.wasSuccess());
mocksControl.verify(); 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() { EasyMock.reportMatcher(new IArgumentMatcher() {
@Override @Override
public boolean matches(Object wptObj2) { public boolean matches(Object object) {
if (wptObj2 == null || wpt == null) return wpt == wptObj2; if (object == null || waypoint == null) {
Waypoint wpt2 = (Waypoint) wptObj2; return waypoint == object;
}
Waypoint waypoint2 = (Waypoint) object;
return wpt.getId() == wpt2.getId(); return waypoint.getId() == waypoint2.getId();
} }
@Override @Override
public void appendTo(StringBuffer buffer) { public void appendTo(StringBuffer buffer) {
buffer.append("wptEq("); buffer.append("waypointEq(");
buffer.append(wpt); buffer.append(waypoint);
buffer.append(")"); buffer.append(")");
} }
}); });
return null; 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() { EasyMock.reportMatcher(new IArgumentMatcher() {
@Override @Override
public boolean matches(Object locObj2) { public boolean matches(Object object) {
if (locObj2 == null || loc == null) return loc == locObj2; if (object == null || location == null) {
Location loc2 = (Location) locObj2; return location == object;
}
Location location2 = (Location) object;
return loc.hasAccuracy() == loc2.hasAccuracy() return location.hasAccuracy() == location2.hasAccuracy()
&& (!loc.hasAccuracy() || loc.getAccuracy() == loc2.getAccuracy()) && (!location.hasAccuracy() || location.getAccuracy() == location2.getAccuracy())
&& loc.hasAltitude() == loc2.hasAltitude() && location.hasAltitude() == location2.hasAltitude()
&& (!loc.hasAltitude() || loc.getAltitude() == loc2.getAltitude()) && (!location.hasAltitude() || location.getAltitude() == location2.getAltitude())
&& loc.hasBearing() == loc2.hasBearing() && location.hasBearing() == location2.hasBearing()
&& (!loc.hasBearing() || loc.getBearing() == loc2.getBearing()) && (!location.hasBearing() || location.getBearing() == location2.getBearing())
&& loc.hasSpeed() == loc2.hasSpeed() && location.hasSpeed() == location2.hasSpeed()
&& (!loc.hasSpeed() || loc.getSpeed() == loc2.getSpeed()) && (!location.hasSpeed() || location.getSpeed() == location2.getSpeed())
&& loc.getLatitude() == loc2.getLatitude() && location.getLatitude() == location2.getLatitude()
&& loc.getLongitude() == loc2.getLongitude() && location.getLongitude() == location2.getLongitude()
&& loc.getTime() == loc2.getTime(); && location.getTime() == location2.getTime();
} }
@Override @Override
public void appendTo(StringBuffer buffer) { public void appendTo(StringBuffer buffer) {
buffer.append("locEq("); buffer.append("locationEq(");
buffer.append(loc); buffer.append(location);
buffer.append(")"); buffer.append(")");
} }
}); });
return null; return null;
} }
private void fillLocations(Location... locs) { /**
assertTrue(locs.length < 90); * Fills the locations.
for (int i = 0; i < locs.length; i++) { *
Location location = locs[i]; * @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.setLatitude(i + 1);
location.setLongitude(i + 1); location.setLongitude(i + 1);
location.setTime(i + 1000); location.setTime(i + 1000);
@@ -16,6 +16,7 @@
package com.google.android.apps.mytracks.endtoendtest; package com.google.android.apps.mytracks.endtoendtest;
import com.google.android.apps.mytracks.TrackListActivity; import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Instrumentation; import android.app.Instrumentation;
@@ -73,8 +74,10 @@ public class DeleteTest extends ActivityInstrumentationTestCase2<TrackListActivi
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO
.clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format), .clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format),
EndToEndTestUtils.GPX.toUpperCase())); EndToEndTestUtils.GPX.toUpperCase()));
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks String totalTracks = activityMyTracks.getResources().getQuantityString(R.plurals.tracks, 0, 0);
.getString(R.string.external_storage_save_error_no_track))); String directoryName = FileUtils.buildExternalDirectoryPath(EndToEndTestUtils.GPX);
String message = activityMyTracks.getString(R.string.save_error, 0, totalTracks, directoryName);
assertTrue(EndToEndTestUtils.SOLO.waitForText(message));
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
} }
@@ -87,16 +87,14 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO
.clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format), .clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format),
EndToEndTestUtils.GPX.toUpperCase())); EndToEndTestUtils.GPX.toUpperCase()));
EndToEndTestUtils.SOLO.waitForText(activityMyTracks EndToEndTestUtils.SOLO.waitForText(getSaveSuccessMessage(1, EndToEndTestUtils.GPX));
.getString(R.string.external_storage_save_success));
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
EndToEndTestUtils.deleteAllTracks(); EndToEndTestUtils.deleteAllTracks();
// Import this tracks. // Import this tracks.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_import), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_import), true);
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString( EndToEndTestUtils.SOLO.waitForText(getImportSuccessMessage(1, EndToEndTestUtils.GPX));
R.string.external_storage_import_success_count).split("%")[0]);
// Check import tracks should be equal with the sum of trackNumber and // Check import tracks should be equal with the sum of trackNumber and
// gpxFilesNumber; // gpxFilesNumber;
EndToEndTestUtils EndToEndTestUtils
@@ -171,9 +169,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
// No file to imported. // No file to imported.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_import), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_import), true);
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString( EndToEndTestUtils.SOLO.waitForText(getImportErrorMessage(0, 0, EndToEndTestUtils.GPX));
R.string.import_no_file,
FileUtils.buildExternalDirectoryPath(EndToEndTestUtils.GPX)));
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
@@ -182,8 +178,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO
.clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format), .clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format),
EndToEndTestUtils.GPX.toUpperCase())); EndToEndTestUtils.GPX.toUpperCase()));
EndToEndTestUtils.SOLO.waitForText(activityMyTracks EndToEndTestUtils.SOLO.waitForText(getSaveSuccessMessage(1, EndToEndTestUtils.GPX));
.getString(R.string.external_storage_save_success));
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Check export file. // Check export file.
@@ -217,8 +212,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO
.clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format), .clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format),
EndToEndTestUtils.KML.toUpperCase())); EndToEndTestUtils.KML.toUpperCase()));
EndToEndTestUtils.SOLO.waitForText(activityMyTracks EndToEndTestUtils.SOLO.waitForText(getSaveSuccessMessage(2, EndToEndTestUtils.KM));
.getString(R.string.external_storage_save_success));
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Check export files. // Check export files.
@@ -232,4 +226,21 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
super.tearDown(); super.tearDown();
} }
private String getSaveSuccessMessage(int count, String type) {
String tracks = activityMyTracks.getResources().getQuantityString(R.plurals.tracks, count, count);
String directoryName = FileUtils.buildExternalDirectoryPath(type);
return activityMyTracks.getString(R.string.save_success, tracks, directoryName);
}
private String getImportSuccessMessage(int count, String type) {
String files = activityMyTracks.getResources().getQuantityString(R.plurals.files, count, count);
String directoryName = FileUtils.buildExternalDirectoryPath(type);
return activityMyTracks.getString(R.string.import_success, files, directoryName);
}
private String getImportErrorMessage(int count, int total, String type) {
String files = activityMyTracks.getResources().getQuantityString(R.plurals.files, total, total);
String directoryName = FileUtils.buildExternalDirectoryPath(type);
return activityMyTracks.getString(R.string.import_error, count, files, directoryName);
}
} }
@@ -246,7 +246,7 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup));
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup_now)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup_now));
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.external_storage_save_success), 0, activityMyTracks.getString(R.string.settings_backup_now_success), 0,
EndToEndTestUtils.SUPER_LONG_WAIT_TIME)); EndToEndTestUtils.SUPER_LONG_WAIT_TIME));
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
@@ -264,7 +264,7 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.restore_success), 0, activityMyTracks.getString(R.string.settings_backup_restore_success), 0,
EndToEndTestUtils.SUPER_LONG_WAIT_TIME)); EndToEndTestUtils.SUPER_LONG_WAIT_TIME));
// Check restore track. // Check restore track.
assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.trackName)); assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.trackName));