From 2f2679c7757923ef3728e61c325ad57103b2c1f2 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 3 Jul 2022 18:23:50 +0200 Subject: [PATCH 1/5] Export: implement export to one file. Fixes #1318 --- src/main/AndroidManifest.xml | 1 + .../opentracks/data/ShareContentProvider.java | 2 +- .../io/file/exporter/CSVTrackExporter.java | 21 +- .../io/file/exporter/ExportActivity.java | 213 ++++++++++-------- .../io/file/exporter/ExportService.java | 20 +- .../exporter/ExportServiceResultReceiver.java | 12 +- .../io/file/exporter/ExportTask.java | 87 +++++++ .../io/file/exporter/GPXTrackExporter.java | 4 +- .../io/file/exporter/KMLTrackExporter.java | 10 +- .../io/file/exporter/KmzTrackExporter.java | 7 +- .../io/file/exporter/TrackExporter.java | 4 +- .../importer/DirectoryChooserActivity.java | 24 ++ .../opentracks/util/ExportUtils.java | 25 +- src/main/res/values/strings.xml | 13 +- src/main/res/xml/settings_import_export.xml | 8 +- 15 files changed, 307 insertions(+), 144 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportTask.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 0ce56c984..d91fff9dd 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -349,6 +349,7 @@ limitations under the License. + diff --git a/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java b/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java index 2e67c145b..1405d7a9b 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java +++ b/src/main/java/de/dennisguse/opentracks/data/ShareContentProvider.java @@ -213,7 +213,7 @@ public class ShareContentProvider extends CustomContentProvider { PipeDataWriter pipeDataWriter = (output, uri1, mimeType, opts, args) -> { try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) { // TODO handle failure (i.e., do not export an empty file) - trackExporter.writeTrack(tracks.toArray(new Track[0]), fileOutputStream); + trackExporter.writeTrack(tracks, fileOutputStream); } catch (IOException e) { Log.w(TAG, "there occurred an error while sharing a file: " + e); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java index 448e54a0f..d50c2e823 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java @@ -88,14 +88,27 @@ public class CSVTrackExporter implements TrackExporter { @Override public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(new Track[]{track}, outputStream); + return writeTrack(List.of(track), outputStream); } @Override - public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) { - List columns = Collections.unmodifiableList(Arrays.asList(new Column("time", null), new Column("trackpoint_type", t -> quote(t.getType().name())), new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""), new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLongitude()) : ""), new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""), new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""), new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""), + public boolean writeTrack(@NonNull List tracks, @NonNull OutputStream outputStream) { + List columns = List.of( + new Column("time", null), + new Column("trackpoint_type", t -> quote(t.getType().name())), + new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""), + new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLongitude()) : ""), + new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""), + new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""), + new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""), - new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""), new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""), new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""), new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""), new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""), new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""), new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : ""))); + new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""), + new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""), + new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""), + new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""), + new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""), + new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""), + new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")); try { prepare(outputStream); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java index 5d988f365..143a9f31d 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java @@ -16,7 +16,6 @@ package de.dennisguse.opentracks.io.file.exporter; -import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -24,6 +23,7 @@ import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.FragmentActivity; @@ -31,11 +31,11 @@ import androidx.fragment.app.FragmentActivity; import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.data.tables.TracksColumns; import de.dennisguse.opentracks.databinding.ExportActivityBinding; import de.dennisguse.opentracks.io.file.ErrorListDialog; import de.dennisguse.opentracks.io.file.TrackFileFormat; @@ -47,12 +47,20 @@ import de.dennisguse.opentracks.util.FileUtils; * An activity for saving tracks to the external storage. * * @author Rodrigo Damazio + * TODO: This class needs some refactoring. + * * It pushes each export job (usually one Track) for export; although export could be run in parallel. + * Also this requires that the ExportActivity stays in foreground, so the user has to activily wait. + * It would be better to let the ExportService handle this and let it report progress / conflicts to ExportActivity + * * File name conflicts are checked in this class instead of the ExportService. + * So, for this check actually a different file name might be used than in the ExportService. + * * Saved state as an object instead of individual values. */ public class ExportActivity extends FragmentActivity implements ExportServiceResultReceiver.Receiver { private static final String TAG = ExportActivity.class.getSimpleName(); public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri"; + public static final String EXTRA_ONE_FILE_KEY = "one_file"; public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat"; private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict"; @@ -64,6 +72,8 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files"; private static final String BUNDLE_TRACK_ERRORS = "track_errors"; + private static final String BUNDLE_EXPORT_TASKS = "export_tasks"; + private static final int CONFLICT_NONE = 0; private static final int CONFLICT_OVERWRITE = 1; private static final int CONFLICT_SKIP = 2; @@ -87,12 +97,13 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private ArrayList trackErrors = new ArrayList<>(); + // TODO Make this an enum private int autoConflict; private ContentProviderUtils contentProviderUtils; // List of tracks to be exported. - private final ArrayList tracks = new ArrayList<>(); + private ArrayList exportTasks; private final LinkedBlockingQueue conflictsQueue = new LinkedBlockingQueue<>(); private final Handler conflictsHandler = new Handler(); @@ -100,34 +111,36 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private final Runnable conflictsRunnable = new Runnable() { @Override public void run() { - if (conflictsQueue.size() > 0) { - PendingConflict conflict = conflictsQueue.peek(); - if (conflict.resolve()) { - conflictsQueue.remove(conflict); - if (!conflictsQueue.isEmpty()) { - conflictsHandler.post(conflictsRunnable); - } - return; - } - - viewBinding.exportProgressLeftButton.setOnClickListener((view) -> { - setConflictVisibility(View.GONE); - conflict.skip(); - conflictsQueue.remove(conflict); - if (!conflictsQueue.isEmpty()) { - conflictsHandler.post(conflictsRunnable); - } - }); - - viewBinding.exportProgressRightButton.setOnClickListener((view) -> { - setConflictVisibility(View.GONE); - conflict.overwrite(); - conflictsQueue.remove(conflict); - if (!conflictsQueue.isEmpty()) { - conflictsHandler.post(conflictsRunnable); - } - }); + if (conflictsQueue.isEmpty()) { + return; } + + PendingConflict conflict = conflictsQueue.peek(); + if (conflict.resolve()) { + conflictsQueue.remove(conflict); + if (!conflictsQueue.isEmpty()) { + conflictsHandler.post(conflictsRunnable); + } + return; + } + + viewBinding.exportProgressLeftButton.setOnClickListener((view) -> { + setConflictVisibility(View.GONE); + conflict.skip(); + conflictsQueue.remove(conflict); + if (!conflictsQueue.isEmpty()) { + conflictsHandler.post(conflictsRunnable); + } + }); + + viewBinding.exportProgressRightButton.setOnClickListener((view) -> { + setConflictVisibility(View.GONE); + conflict.overwrite(); + conflictsQueue.remove(conflict); + if (!conflictsQueue.isEmpty()) { + conflictsHandler.post(conflictsRunnable); + } + }); } }; @@ -139,6 +152,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY); trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY); + boolean allInOneFile = getIntent().getBooleanExtra(EXTRA_ONE_FILE_KEY, false); contentProviderUtils = new ContentProviderUtils(this); @@ -152,7 +166,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes setProgress(); new Thread(() -> { directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri()); - runOnUiThread(() -> initExport(0)); + runOnUiThread(() -> { + createExportTasks(allInOneFile); + nextExport(null); + }); }).start(); } else { autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT); @@ -163,9 +180,9 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT); directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES); trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS); - + exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS)); setProgress(); - initExport(getTotalDone()); + nextExport(null); } viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName)); @@ -183,13 +200,14 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount); outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList) directoryFiles); outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors); + outState.putParcelableArrayList(BUNDLE_EXPORT_TASKS, exportTasks); } @Override protected void onDestroy() { super.onDestroy(); conflictsQueue.clear(); - tracks.clear(); + exportTasks.clear(); } @Override @@ -202,57 +220,50 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes this.doubleBackToCancel = true; Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show(); - new Handler().postDelayed(() -> doubleBackToCancel=false, 2000); + new Handler().postDelayed(() -> doubleBackToCancel = false, 2000); } - private void initExport(int from) { - try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) { - if (cursor == null) { - onExportEnded(); - return; - } - - trackExportTotalCount = cursor.getCount(); - viewBinding.exportProgressTotal.setText("" + trackExportTotalCount); - for (int i = from; i < trackExportTotalCount; i++) { - cursor.moveToPosition(i); - Track track = ContentProviderUtils.createTrack(cursor); - tracks.add(track); - } - - if (!tracks.isEmpty()) { - export(tracks.get(0)); - } else { - onExportEnded(); - } + private void createExportTasks(boolean allInOneFile) { + List tracks = contentProviderUtils.getTracks(); + exportTasks = new ArrayList<>(); + if (allInOneFile) { + String filename = "OpenTracks-Backup"; + exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList()))); + } else { + exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList())); } + trackExportTotalCount = exportTasks.size(); } /** * Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution. - * - * @param track Track object. - * @param conflictResolution conflict resolution to be applied if needed. */ - private void export(Track track, int conflictResolution) { - boolean fileExists = exportFileExists(track); + private void export(ExportTask exportTask, int conflictResolution) { + boolean fileExists = exportFileExists(exportTask); if (fileExists && conflictResolution == CONFLICT_NONE) { - conflict(track); + conflict(exportTask); } else if (fileExists && conflictResolution == CONFLICT_SKIP) { trackExportSkippedCount++; - onExportCompleted(track); + nextExport(exportTask); } else { - ExportService.enqueue(this, resultReceiver, track.getId(), trackFileFormat, directoryUri); + ExportService.enqueue(this, resultReceiver, exportTask, directoryUri); } } - private void export(Track track) { - export(track, autoConflict); + private void export(ExportTask exportTask) { + export(exportTask, autoConflict); } - private boolean exportFileExists(Track track) { - String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); + @Deprecated //TODO Check should be done in ExportService + private boolean exportFileExists(ExportTask exportTask) { + String filename; + if (exportTask.isMultiExport()) { + filename = exportTask.getFilename(); + } else { + Track track = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)); + filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); + } return directoryFiles.stream().anyMatch(filename::equals); } @@ -269,31 +280,29 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes } private void setProgress() { - int done = getTotalDone(); - - viewBinding.exportProgressDone.setText("" + done); + viewBinding.exportProgressDone.setText("" + getTotalDone()); viewBinding.exportProgressTotal.setText("" + trackExportTotalCount); - viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f)); - viewBinding.exportProgressSummaryNew.setText(String.valueOf(trackExportSuccessCount)); - viewBinding.exportProgressSummaryOverwrite.setText(String.valueOf(trackExportOverwrittenCount)); - viewBinding.exportProgressSummarySkip.setText(String.valueOf(trackExportSkippedCount)); - viewBinding.exportProgressSummaryErrors.setText(String.valueOf(trackExportErrorCount)); + viewBinding.exportProgressBar.setProgress((int) ((float) getTotalDone() / (float) trackExportTotalCount * 100f)); + viewBinding.exportProgressSummaryNew.setText("" + trackExportSuccessCount); + viewBinding.exportProgressSummaryOverwrite.setText("" + trackExportOverwrittenCount); + viewBinding.exportProgressSummarySkip.setText("" + trackExportSkippedCount); + viewBinding.exportProgressSummaryErrors.setText("" + trackExportErrorCount); viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE); } - private void onExportCompleted(Track track) { - tracks.remove(track); + private void nextExport(@Nullable ExportTask exportTask) { + exportTasks.remove(exportTask); setProgress(); - if (tracks.isEmpty()) { + if (exportTasks.isEmpty()) { onExportEnded(); return; } - export(tracks.get(0)); + export(exportTasks.get(0)); } private void onExportEnded() { @@ -318,30 +327,32 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes } @Override - public void onExportSuccess(Track.Id trackId) { - Track track = contentProviderUtils.getTrack(trackId); - - if (exportFileExists(track)) { + public void onExportSuccess(ExportTask exportTask) { + if (exportFileExists(exportTask)) { trackExportOverwrittenCount++; } else { trackExportSuccessCount++; } - onExportCompleted(track); + nextExport(exportTask); } @Override - public void onExportError(Track.Id trackId) { - Track track = contentProviderUtils.getTrack(trackId); - + public void onExportError(ExportTask exportTask) { trackExportErrorCount++; - trackErrors.add(track.getName()); + String name; + if (exportTask.isMultiExport()) { + name = exportTask.getFilename(); + } else { + name = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)).getName(); + } + trackErrors.add(name); - onExportCompleted(track); + nextExport(exportTask); } - private void conflict(Track track) { - PendingConflict newConflict = new PendingConflict(track); + private void conflict(ExportTask exportTask) { + PendingConflict newConflict = new PendingConflict(exportTask); conflictsQueue.add(newConflict); if (conflictsQueue.size() == 1) { @@ -353,10 +364,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * Handle conflicts (exporting file already exists). */ private class PendingConflict { - private final Track track; + private final ExportTask exportTask; - public PendingConflict(Track track) { - this.track = track; + public PendingConflict(ExportTask exportTask) { + this.exportTask = exportTask; } /** @@ -368,12 +379,18 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes public boolean resolve() { if (autoConflict == CONFLICT_NONE) { viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(ExportActivity.this, R.drawable.ic_report_problem_24)); - viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, track.getName())); + String name; + if (exportTask.isMultiExport()) { + name = exportTask.getFilename(); + } else { + name = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)).getName(); + } + viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, name)); setConflictVisibility(View.VISIBLE); return false; } - export(track); + export(exportTask); return true; } @@ -381,7 +398,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * Overwrite the export file and set the autoConflict if user set the "do it for all" switch button. */ public void overwrite() { - export(track, CONFLICT_OVERWRITE); + export(exportTask, CONFLICT_OVERWRITE); if (viewBinding.exportProgressApplyToAll.isChecked()) { autoConflict = CONFLICT_OVERWRITE; @@ -392,7 +409,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * Skip the export file and set the autoConflict if user set the "do it for all" switch button. */ public void skip() { - export(track, CONFLICT_SKIP); + export(exportTask, CONFLICT_SKIP); if (viewBinding.exportProgressApplyToAll.isChecked()) { autoConflict = CONFLICT_SKIP; diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java index 68e887c9f..ac4180776 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java @@ -12,6 +12,9 @@ import androidx.annotation.NonNull; import androidx.core.app.JobIntentService; import androidx.documentfile.provider.DocumentFile; +import java.util.List; +import java.util.stream.Collectors; + import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.io.file.TrackFileFormat; @@ -22,16 +25,14 @@ public class ExportService extends JobIntentService { private static final int JOB_ID = 1; private static final String EXTRA_RECEIVER = "extra_receiver"; - private static final String EXTRA_TRACK_ID = "extra_track_id"; - private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format"; + private static final String EXTRA_EXPORT_TASK = "export_task"; private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri"; private static final String TAG = ExportService.class.getSimpleName(); - public static void enqueue(Context context, ExportServiceResultReceiver receiver, Track.Id trackId, TrackFileFormat trackFileFormat, Uri directoryUri) { + public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) { Intent intent = new Intent(context, JobService.class); intent.putExtra(EXTRA_RECEIVER, receiver); - intent.putExtra(EXTRA_TRACK_ID, trackId); - intent.putExtra(EXTRA_TRACK_FILE_FORMAT, trackFileFormat); + intent.putExtra(EXTRA_EXPORT_TASK, exportTask); intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri); enqueueWork(context, ExportService.class, JOB_ID, intent); } @@ -40,13 +41,12 @@ public class ExportService extends JobIntentService { protected void onHandleWork(@NonNull Intent intent) { // Get all data. ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER); - Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID); - TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT); + ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK); Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI); // Prepare resultCode and bundle to send to the receiver. Bundle bundle = new Bundle(); - bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId); + bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask); // Build directory file. DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri); @@ -57,9 +57,7 @@ public class ExportService extends JobIntentService { } // Export. - ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); - Track track = contentProviderUtils.getTrack(trackId); - boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track); + boolean success = ExportUtils.exportTrack(this, directoryFile, exportTask); // Send result to the receiver. int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR; diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportServiceResultReceiver.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportServiceResultReceiver.java index f369de924..d06a7dcf6 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportServiceResultReceiver.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportServiceResultReceiver.java @@ -17,7 +17,7 @@ public class ExportServiceResultReceiver extends ResultReceiver { public static final int RESULT_CODE_SUCCESS = 1; public static final int RESULT_CODE_ERROR = 0; - public static final String RESULT_EXTRA_TRACK_ID = "result_extra_track_id"; + public static final String RESULT_EXTRA_EXPORT_TASK = "result_extra_export_task"; private final Receiver receiver; @@ -28,19 +28,19 @@ public class ExportServiceResultReceiver extends ResultReceiver { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - Track.Id trackId = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID); + ExportTask exportTask = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK); switch (resultCode) { - case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(trackId); - case RESULT_CODE_ERROR -> receiver.onExportError(trackId); + case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask); + case RESULT_CODE_ERROR -> receiver.onExportError(exportTask); default -> throw new RuntimeException("Unknown resultCode."); } } public interface Receiver { - default void onExportSuccess(Track.Id trackId) { + default void onExportSuccess(ExportTask exportTask) { } - default void onExportError(Track.Id trackId) { + default void onExportError(ExportTask exportTask) { } } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportTask.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportTask.java new file mode 100644 index 000000000..b4066223c --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportTask.java @@ -0,0 +1,87 @@ +package de.dennisguse.opentracks.io.file.exporter; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.io.file.TrackFileFormat; + +public class ExportTask implements Parcelable { + + private final String filename; //TODO Name will only be used when exporting multiple tracks + + private final TrackFileFormat trackFileFormat; + + private final List trackIds; + + public ExportTask(@Nullable String filename, @NonNull TrackFileFormat trackFileFormat, @NonNull List trackIds) { + this.filename = filename; + this.trackFileFormat = trackFileFormat; + this.trackIds = trackIds; + } + + protected ExportTask(Parcel in) { + filename = in.readString(); + trackFileFormat = TrackFileFormat.valueOf(in.readString()); + trackIds = in.createTypedArrayList(Track.Id.CREATOR); + } + + public String getFilename() { + return filename; + } + + public TrackFileFormat getTrackFileFormat() { + return trackFileFormat; + } + + public List getTrackIds() { + return trackIds; + } + + public boolean isMultiExport() { + return trackIds.size() > 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExportTask that = (ExportTask) o; + return Objects.equals(filename, that.filename) && trackFileFormat == that.trackFileFormat && Objects.equals(trackIds, that.trackIds); + } + + @Override + public int hashCode() { + return Objects.hash(filename, trackFileFormat, trackIds); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(filename); + dest.writeString(trackFileFormat.name()); + dest.writeTypedList(trackIds); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public ExportTask createFromParcel(Parcel in) { + return new ExportTask(in); + } + + @Override + public ExportTask[] newArray(int size) { + return new ExportTask[size]; + } + }; +} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java index 187c7d7b5..e342de050 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java @@ -99,11 +99,11 @@ public class GPXTrackExporter implements TrackExporter { @Override public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(new Track[]{track}, outputStream); + return writeTrack(List.of(track), outputStream); } @Override - public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) { + public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try { prepare(outputStream); writeHeader(); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java index a67a60734..f3272a031 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java @@ -100,17 +100,17 @@ public class KMLTrackExporter implements TrackExporter { } public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(new Track[]{track}, outputStream); + return writeTrack(List.of(track), outputStream); } - public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) { + public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try { prepare(outputStream); writeHeader(tracks); for (Track track : tracks) { writeMarkers(track); } - boolean hasMultipleTracks = tracks.length > 1; + boolean hasMultipleTracks = tracks.size() > 1; if (hasMultipleTracks) { writeMultiTrackBegin(); } @@ -222,7 +222,7 @@ public class KMLTrackExporter implements TrackExporter { } } - private void writeHeader(Track[] tracks) { + private void writeHeader(List tracks) { if (printWriter != null) { printWriter.println( """ @@ -241,7 +241,7 @@ public class KMLTrackExporter implements TrackExporter { printWriter.println("1"); printWriter.println("1"); - Track track = tracks[0]; + Track track = tracks.get(0); printWriter.println("" + StringUtils.formatCData(track.getName()) + ""); printWriter.println("" + StringUtils.formatCData(context.getString(R.string.app_name)) + ""); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java index 99c59f0e0..f29c859af 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java @@ -28,6 +28,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -65,11 +66,11 @@ public class KmzTrackExporter implements TrackExporter { @Override public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(new Track[]{track}, outputStream); + return writeTrack(List.of(track), outputStream); } @Override - public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) { + public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { // Add kml file ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE); @@ -91,7 +92,7 @@ public class KmzTrackExporter implements TrackExporter { } } - private void addImages(Context context, Track[] tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException { + private void addImages(Context context, List tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException { for (Track track : tracks) { try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) { if (cursor != null && cursor.moveToFirst()) { diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java index a73a93235..bf5e138e2 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java @@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.exporter; import androidx.annotation.NonNull; import java.io.OutputStream; +import java.util.List; import de.dennisguse.opentracks.data.models.Track; @@ -29,7 +30,8 @@ import de.dennisguse.opentracks.data.models.Track; */ public interface TrackExporter { - boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream); + boolean writeTrack(List tracks, @NonNull OutputStream outputStream); + @Deprecated // Only used for tests boolean writeTrack(Track track, @NonNull OutputStream outputStream); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java index a56b8f0e0..38b997e1f 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/DirectoryChooserActivity.java @@ -101,6 +101,30 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity { } } + public static class ExportDirectoryChooserOneFileActivity extends DirectoryChooserActivity { + + @Override + protected DocumentFile configureDirectoryChooserIntent(Intent intent) { + super.configureDirectoryChooserIntent(intent); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + return IntentUtils.toDocumentFile(this, PreferencesUtils.getDefaultExportDirectoryUri()); + } + + @Override + protected boolean isDirectoryValid(final DocumentFile directoryUri) { + return super.isDirectoryValid(directoryUri) && directoryUri.canWrite(); + } + + @Override + protected Intent createNextActivityIntent(Uri directoryUri) { + Intent intent = IntentUtils.newIntent(this, ExportActivity.class); + intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri); + intent.putExtra(ExportActivity.EXTRA_ONE_FILE_KEY, true); + intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat()); + return intent; + } + } + public static class DefaultTrackExportDirectoryChooserActivity extends DirectoryChooserActivity { @Override diff --git a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java index eedde3777..aa97e0f13 100644 --- a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; @@ -24,6 +25,7 @@ import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.exporter.ExportService; import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver; +import de.dennisguse.opentracks.io.file.exporter.ExportTask; import de.dennisguse.opentracks.io.file.exporter.TrackExporter; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.SettingsActivity; @@ -44,7 +46,7 @@ public class ExportUtils { ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() { @Override - public void onExportError(Track.Id trackId) { + public void onExportError(ExportTask unused) { Intent intent = new Intent(context, SettingsActivity.class); intent.putExtra(SettingsActivity.EXTRAS_CHECK_EXPORT_DIRECTORY, true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -52,21 +54,28 @@ public class ExportUtils { } }); - ExportService.enqueue(context, resultReceiver, trackId, trackFileFormat, directory.getUri()); + ExportService.enqueue(context, resultReceiver, new ExportTask(null, trackFileFormat, List.of(trackId)), directory.getUri()); } } - public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) { - TrackExporter trackExporter = trackFileFormat.createTrackExporter(context, new ContentProviderUtils(context)); + public static boolean exportTrack(Context context, DocumentFile directory, ExportTask exportTask) { + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + List tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList()); + Uri exportDocumentFileUri; + if (tracks.size() == 1) { + exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), exportTask.getTrackFileFormat(), directory); + } else { + exportDocumentFileUri = getExportDocumentFileUri(context, exportTask.getFilename(), exportTask.getTrackFileFormat(), directory); + } - Uri exportDocumentFileUri = getExportDocumentFileUri(context, track, trackFileFormat, directory); if (exportDocumentFileUri == null) { Log.e(TAG, "Couldn't create document file for export"); return false; } + TrackExporter trackExporter = exportTask.getTrackFileFormat().createTrackExporter(context, contentProviderUtils); try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) { - if (trackExporter.writeTrack(track, outputStream)) { + if (trackExporter.writeTrack(tracks, outputStream)) { return true; } else { if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) { @@ -102,6 +111,10 @@ public class ExportUtils { private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) { String exportFileName = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); + return getExportDocumentFileUri(context, exportFileName, trackFileFormat, directory); + } + + private static Uri getExportDocumentFileUri(Context context, String exportFileName, TrackFileFormat trackFileFormat, DocumentFile directory) { Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName); if (exportDocumentFileUri == null) { final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cf47f8a9a..d3f906515 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -210,12 +210,12 @@ limitations under the License. Cannot write to export directory. " (not writable!)" Exporting to %1$s… - The track %1$s already exists at the destination directory. + The file %1$s already exists at the destination directory. do it for all conflicts - Tracks not exported - New tracks exported - Tracks overwritten - Tracks skipped + Files not exported + New files exported + Files overwritten + Files skipped Errors @@ -297,7 +297,8 @@ limitations under the License. Aggregated stats Delete Edit - Export all + Export all (one file per track) + Export all (one file for all tracks) Help Import all Insert marker diff --git a/src/main/res/xml/settings_import_export.xml b/src/main/res/xml/settings_import_export.xml index 598816609..51b92128f 100644 --- a/src/main/res/xml/settings_import_export.xml +++ b/src/main/res/xml/settings_import_export.xml @@ -26,6 +26,13 @@ android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" android:targetPackage="@string/applicationId" /> + + + @@ -48,5 +55,4 @@ android:title="@string/settings_export_filename_title" android:defaultValue="@string/export_filename_format_default" /> - \ No newline at end of file From 3aadf6d5d831ad6ebc58f27821206c4839b5175b Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 21 May 2023 23:42:53 +0200 Subject: [PATCH 2/5] Cleanup: use an enum for export's conflict resolution. --- .../io/file/exporter/ExportActivity.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java index 143a9f31d..69444e4b0 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java @@ -74,9 +74,11 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private static final String BUNDLE_EXPORT_TASKS = "export_tasks"; - private static final int CONFLICT_NONE = 0; - private static final int CONFLICT_OVERWRITE = 1; - private static final int CONFLICT_SKIP = 2; + private enum ConflictResolutionStrategy { + CONFLICT_NONE, + CONFLICT_OVERWRITE, + CONFLICT_SKIP; + } private TrackFileFormat trackFileFormat; private Uri directoryUri; @@ -97,8 +99,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private ArrayList trackErrors = new ArrayList<>(); - // TODO Make this an enum - private int autoConflict; + private ConflictResolutionStrategy autoConflict; private ContentProviderUtils contentProviderUtils; @@ -162,7 +163,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes resultReceiver = new ExportServiceResultReceiver(new Handler(), this); if (savedInstanceState == null) { - autoConflict = CONFLICT_NONE; + autoConflict = ConflictResolutionStrategy.CONFLICT_NONE; setProgress(); new Thread(() -> { directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri()); @@ -172,7 +173,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes }); }).start(); } else { - autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT); + autoConflict = ConflictResolutionStrategy.valueOf(savedInstanceState.getString(BUNDLE_AUTO_CONFLICT)); trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT); trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT); trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT); @@ -192,7 +193,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putInt(BUNDLE_AUTO_CONFLICT, autoConflict); + outState.putString(BUNDLE_AUTO_CONFLICT, autoConflict.name()); outState.putInt(BUNDLE_SUCCESS_COUNT, trackExportSuccessCount); outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount); outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount); @@ -238,12 +239,12 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes /** * Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution. */ - private void export(ExportTask exportTask, int conflictResolution) { + private void export(ExportTask exportTask, ConflictResolutionStrategy conflictResolution) { boolean fileExists = exportFileExists(exportTask); - if (fileExists && conflictResolution == CONFLICT_NONE) { + if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) { conflict(exportTask); - } else if (fileExists && conflictResolution == CONFLICT_SKIP) { + } else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) { trackExportSkippedCount++; nextExport(exportTask); } else { @@ -377,7 +378,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * @return true if it could resolve the conflict or false otherwise. */ public boolean resolve() { - if (autoConflict == CONFLICT_NONE) { + if (autoConflict == ConflictResolutionStrategy.CONFLICT_NONE) { viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(ExportActivity.this, R.drawable.ic_report_problem_24)); String name; if (exportTask.isMultiExport()) { @@ -398,10 +399,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * Overwrite the export file and set the autoConflict if user set the "do it for all" switch button. */ public void overwrite() { - export(exportTask, CONFLICT_OVERWRITE); + export(exportTask, ConflictResolutionStrategy.CONFLICT_OVERWRITE); if (viewBinding.exportProgressApplyToAll.isChecked()) { - autoConflict = CONFLICT_OVERWRITE; + autoConflict = ConflictResolutionStrategy.CONFLICT_OVERWRITE; } } @@ -409,10 +410,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes * Skip the export file and set the autoConflict if user set the "do it for all" switch button. */ public void skip() { - export(exportTask, CONFLICT_SKIP); + export(exportTask, ConflictResolutionStrategy.CONFLICT_SKIP); if (viewBinding.exportProgressApplyToAll.isChecked()) { - autoConflict = CONFLICT_SKIP; + autoConflict = ConflictResolutionStrategy.CONFLICT_SKIP; } } } From f48c4806b5bdf3602ca9d355654cbc5bf0396509 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Wed, 24 May 2023 20:12:04 +0200 Subject: [PATCH 3/5] Simplify TrackExporter interface. --- .../opentracks/io/file/importer/ExportImportTest.java | 10 +++++----- .../io/file/importer/GPXTrackImporterTest.java | 2 +- .../opentracks/io/file/exporter/CSVTrackExporter.java | 7 ------- .../opentracks/io/file/exporter/GPXTrackExporter.java | 5 ----- .../opentracks/io/file/exporter/KMLTrackExporter.java | 4 ---- .../opentracks/io/file/exporter/KmzTrackExporter.java | 5 ----- .../opentracks/io/file/exporter/TrackExporter.java | 3 --- 7 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index 2adbc1096..bdaf92f5c 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -208,7 +208,7 @@ public class ExportImportTest { // when // 1. export - trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri)); contentProviderUtils.deleteTrack(context, trackId); // 2. import @@ -290,7 +290,7 @@ public class ExportImportTest { TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, contentProviderUtils); // when - trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri)); // 2. import InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); @@ -314,7 +314,7 @@ public class ExportImportTest { // when // 1. export - trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri)); contentProviderUtils.deleteTrack(context, trackId); // 2. import @@ -432,7 +432,7 @@ public class ExportImportTest { // when // 1. export - trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri)); // 2. import InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); @@ -457,7 +457,7 @@ public class ExportImportTest { // when // 1. export - trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); + trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri)); contentProviderUtils.deleteTrack(context, trackId); // then diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/GPXTrackImporterTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/GPXTrackImporterTest.java index 55887c202..256e9ecfa 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/GPXTrackImporterTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/GPXTrackImporterTest.java @@ -232,7 +232,7 @@ public class GPXTrackImporterTest { Track importedTrack = contentProviderUtils.getTrack(importTrackId); TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils); - trackExporter.writeTrack(importedTrack, outputStream); + trackExporter.writeTrack(List.of(importedTrack), outputStream); // then String expected = new BufferedReader( diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java index d50c2e823..1053ccebf 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/CSVTrackExporter.java @@ -23,8 +23,6 @@ import androidx.annotation.NonNull; import java.io.OutputStream; import java.io.PrintWriter; import java.text.NumberFormat; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Function; @@ -86,11 +84,6 @@ public class CSVTrackExporter implements TrackExporter { this.contentProviderUtils = contentProviderUtils; } - @Override - public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(List.of(track), outputStream); - } - @Override public boolean writeTrack(@NonNull List tracks, @NonNull OutputStream outputStream) { List columns = List.of( diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java index e342de050..34e1acfd9 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GPXTrackExporter.java @@ -97,11 +97,6 @@ public class GPXTrackExporter implements TrackExporter { this.creator = creator; } - @Override - public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(List.of(track), outputStream); - } - @Override public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try { diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java index f3272a031..e0ab40bd9 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java @@ -99,10 +99,6 @@ public class KMLTrackExporter implements TrackExporter { this.contentProviderUtils = contentProviderUtils; } - public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(List.of(track), outputStream); - } - public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try { prepare(outputStream); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java index f29c859af..67cc5c52b 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java @@ -64,11 +64,6 @@ public class KmzTrackExporter implements TrackExporter { this.exportPhotos = exportPhotos; } - @Override - public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { - return writeTrack(List.of(track), outputStream); - } - @Override public boolean writeTrack(List tracks, @NonNull OutputStream outputStream) { try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java index bf5e138e2..0d24e9299 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/TrackExporter.java @@ -31,7 +31,4 @@ import de.dennisguse.opentracks.data.models.Track; public interface TrackExporter { boolean writeTrack(List tracks, @NonNull OutputStream outputStream); - - @Deprecated // Only used for tests - boolean writeTrack(Track track, @NonNull OutputStream outputStream); } From ccc18739a4950970b0e5ea99b0586c8e08538a92 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 28 May 2023 21:04:32 +0200 Subject: [PATCH 4/5] Bugfix: KML exporter for multiple tracks created invalid XML. Part of #1318. --- .../opentracks/io/file/exporter/KMLTrackExporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java index e0ab40bd9..702914eb8 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java @@ -289,7 +289,7 @@ public class KMLTrackExporter implements TrackExporter { private void writeMultiTrackBegin() { if (printWriter != null) { - printWriter.println(""); + printWriter.println(""); printWriter.println("" + context.getString(R.string.generic_tracks) + ""); printWriter.println("1"); } From ae37ec9c33b46e29de85d3b728bce5a75bb46e58 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 28 May 2023 21:20:04 +0200 Subject: [PATCH 5/5] Bugfix: set file suffix for "one file"-exports explicitly. KML/KMZ was added by AndroidOS, but not GPX. Part of #1318. --- .../opentracks/io/file/TrackFilenameGenerator.java | 10 +++++----- .../opentracks/io/file/exporter/ExportActivity.java | 3 ++- .../de/dennisguse/opentracks/util/ExportUtils.java | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/dennisguse/opentracks/io/file/TrackFilenameGenerator.java b/src/main/java/de/dennisguse/opentracks/io/file/TrackFilenameGenerator.java index eeb5dccbc..2f80c1fe1 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/TrackFilenameGenerator.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/TrackFilenameGenerator.java @@ -29,6 +29,10 @@ public class TrackFilenameGenerator { .collect(Collectors.joining(", ")); } + public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) { + return FileUtils.sanitizeFileName(name + "." + trackFileFormat.getExtension()); + } + private final String template; public TrackFilenameGenerator(@NonNull String template) { @@ -44,7 +48,7 @@ public class TrackFilenameGenerator { values.put(STARTTIME_TIME_KEY, track.getStartTime().toLocalTime().toString()); values.put(STARTTIME_DATE_KEY, track.getStartTime().toLocalDate().toString()); - return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension(); + return format(format(template, values), trackFileFormat); } private static String format(String template, Map values) { @@ -77,10 +81,6 @@ public class TrackFilenameGenerator { return String.format(templateCompiled, valueList.toArray()); } - public String getTemplate() { - return template; - } - public boolean isValid() { try { getExample(); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java index 69444e4b0..c1d33da3c 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java @@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.databinding.ExportActivityBinding; import de.dennisguse.opentracks.io.file.ErrorListDialog; import de.dennisguse.opentracks.io.file.TrackFileFormat; +import de.dennisguse.opentracks.io.file.TrackFilenameGenerator; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.FileUtils; @@ -260,7 +261,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes private boolean exportFileExists(ExportTask exportTask) { String filename; if (exportTask.isMultiExport()) { - filename = exportTask.getFilename(); + filename = TrackFilenameGenerator.format(exportTask.getFilename(), exportTask.getTrackFileFormat()); } else { Track track = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0)); filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); diff --git a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java index aa97e0f13..3f65832aa 100644 --- a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java @@ -23,6 +23,7 @@ import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.io.file.TrackFileFormat; +import de.dennisguse.opentracks.io.file.TrackFilenameGenerator; import de.dennisguse.opentracks.io.file.exporter.ExportService; import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver; import de.dennisguse.opentracks.io.file.exporter.ExportTask; @@ -65,7 +66,8 @@ public class ExportUtils { if (tracks.size() == 1) { exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), exportTask.getTrackFileFormat(), directory); } else { - exportDocumentFileUri = getExportDocumentFileUri(context, exportTask.getFilename(), exportTask.getTrackFileFormat(), directory); + String filename = TrackFilenameGenerator.format(exportTask.getFilename(), exportTask.getTrackFileFormat()); + exportDocumentFileUri = getExportDocumentFileUri(context, filename, exportTask.getTrackFileFormat(), directory); } if (exportDocumentFileUri == null) {