Export all tracks into one file
This commit is contained in:
Dennis Guse
2023-05-29 09:08:13 +02:00
committed by GitHub
18 changed files with 330 additions and 187 deletions
@@ -208,7 +208,7 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
@@ -290,7 +290,7 @@ public class ExportImportTest {
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, contentProviderUtils); TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.createTrackExporter(context, contentProviderUtils);
// when // when
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import // 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -314,7 +314,7 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// 2. import // 2. import
@@ -432,7 +432,7 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
// 2. import // 2. import
InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri); InputStream inputStream = context.getContentResolver().openInputStream(tmpFileUri);
@@ -457,7 +457,7 @@ public class ExportImportTest {
// when // when
// 1. export // 1. export
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri)); trackExporter.writeTrack(List.of(track), context.getContentResolver().openOutputStream(tmpFileUri));
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
// then // then
@@ -232,7 +232,7 @@ public class GPXTrackImporterTest {
Track importedTrack = contentProviderUtils.getTrack(importTrackId); Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils); TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context, contentProviderUtils);
trackExporter.writeTrack(importedTrack, outputStream); trackExporter.writeTrack(List.of(importedTrack), outputStream);
// then // then
String expected = new BufferedReader( String expected = new BufferedReader(
+1
View File
@@ -349,6 +349,7 @@ limitations under the License.
<activity android:name=".ui.aggregatedStatistics.AggregatedStatisticsActivity" /> <activity android:name=".ui.aggregatedStatistics.AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" /> <activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" /> <activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
<activity android:name=".io.file.exporter.ExportActivity" /> <activity android:name=".io.file.exporter.ExportActivity" />
@@ -213,7 +213,7 @@ public class ShareContentProvider extends CustomContentProvider {
PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> { PipeDataWriter<String> pipeDataWriter = (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) { try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
// TODO handle failure (i.e., do not export an empty file) // 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) { } catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e); Log.w(TAG, "there occurred an error while sharing a file: " + e);
} }
@@ -29,6 +29,10 @@ public class TrackFilenameGenerator {
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
} }
public static String format(@NonNull String name, @NonNull TrackFileFormat trackFileFormat) {
return FileUtils.sanitizeFileName(name + "." + trackFileFormat.getExtension());
}
private final String template; private final String template;
public TrackFilenameGenerator(@NonNull 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_TIME_KEY, track.getStartTime().toLocalTime().toString());
values.put(STARTTIME_DATE_KEY, track.getStartTime().toLocalDate().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<String, String> values) { private static String format(String template, Map<String, String> values) {
@@ -77,10 +81,6 @@ public class TrackFilenameGenerator {
return String.format(templateCompiled, valueList.toArray()); return String.format(templateCompiled, valueList.toArray());
} }
public String getTemplate() {
return template;
}
public boolean isValid() { public boolean isValid() {
try { try {
getExample(); getExample();
@@ -23,8 +23,6 @@ import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.Function; import java.util.function.Function;
@@ -87,15 +85,23 @@ public class CSVTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream); List<Column> 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()) : ""),
@Override new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) { new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
List<Column> 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()) : ""), 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("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("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 { try {
prepare(outputStream); prepare(outputStream);
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.exporter; package de.dennisguse.opentracks.io.file.exporter;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@@ -24,6 +23,7 @@ import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
@@ -31,14 +31,15 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding; import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog; import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils; import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
@@ -47,12 +48,20 @@ import de.dennisguse.opentracks.util.FileUtils;
* An activity for saving tracks to the external storage. * An activity for saving tracks to the external storage.
* *
* @author Rodrigo Damazio * @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 { public class ExportActivity extends FragmentActivity implements ExportServiceResultReceiver.Receiver {
private static final String TAG = ExportActivity.class.getSimpleName(); private static final String TAG = ExportActivity.class.getSimpleName();
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri"; 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"; public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict"; private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict";
@@ -64,9 +73,13 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files"; private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files";
private static final String BUNDLE_TRACK_ERRORS = "track_errors"; private static final String BUNDLE_TRACK_ERRORS = "track_errors";
private static final int CONFLICT_NONE = 0; private static final String BUNDLE_EXPORT_TASKS = "export_tasks";
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 TrackFileFormat trackFileFormat;
private Uri directoryUri; private Uri directoryUri;
@@ -87,12 +100,12 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private ArrayList<String> trackErrors = new ArrayList<>(); private ArrayList<String> trackErrors = new ArrayList<>();
private int autoConflict; private ConflictResolutionStrategy autoConflict;
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
// List of tracks to be exported. // List of tracks to be exported.
private final ArrayList<Track> tracks = new ArrayList<>(); private ArrayList<ExportTask> exportTasks;
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>(); private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
private final Handler conflictsHandler = new Handler(); private final Handler conflictsHandler = new Handler();
@@ -100,34 +113,36 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
private final Runnable conflictsRunnable = new Runnable() { private final Runnable conflictsRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
if (conflictsQueue.size() > 0) { if (conflictsQueue.isEmpty()) {
PendingConflict conflict = conflictsQueue.peek(); return;
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);
}
});
} }
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 +154,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY); directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY); trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
boolean allInOneFile = getIntent().getBooleanExtra(EXTRA_ONE_FILE_KEY, false);
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
@@ -148,14 +164,17 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
resultReceiver = new ExportServiceResultReceiver(new Handler(), this); resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
if (savedInstanceState == null) { if (savedInstanceState == null) {
autoConflict = CONFLICT_NONE; autoConflict = ConflictResolutionStrategy.CONFLICT_NONE;
setProgress(); setProgress();
new Thread(() -> { new Thread(() -> {
directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri()); directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
runOnUiThread(() -> initExport(0)); runOnUiThread(() -> {
createExportTasks(allInOneFile);
nextExport(null);
});
}).start(); }).start();
} else { } else {
autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT); autoConflict = ConflictResolutionStrategy.valueOf(savedInstanceState.getString(BUNDLE_AUTO_CONFLICT));
trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT); trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT);
trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT); trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT);
trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT); trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT);
@@ -163,9 +182,9 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT); trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES); directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS); trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
exportTasks = new ArrayList<>(savedInstanceState.getParcelableArrayList(BUNDLE_EXPORT_TASKS));
setProgress(); setProgress();
initExport(getTotalDone()); nextExport(null);
} }
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName)); viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName));
@@ -175,7 +194,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
@Override @Override
protected void onSaveInstanceState(@NonNull Bundle outState) { protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(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_SUCCESS_COUNT, trackExportSuccessCount);
outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount); outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount);
outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount); outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount);
@@ -183,13 +202,14 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount); outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles); outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles);
outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors); outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors);
outState.putParcelableArrayList(BUNDLE_EXPORT_TASKS, exportTasks);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
conflictsQueue.clear(); conflictsQueue.clear();
tracks.clear(); exportTasks.clear();
} }
@Override @Override
@@ -202,57 +222,50 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
this.doubleBackToCancel = true; this.doubleBackToCancel = true;
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show(); 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) { private void createExportTasks(boolean allInOneFile) {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) { List<Track> tracks = contentProviderUtils.getTracks();
if (cursor == null) { exportTasks = new ArrayList<>();
onExportEnded(); if (allInOneFile) {
return; String filename = "OpenTracks-Backup";
} exportTasks.add(new ExportTask(filename, trackFileFormat, tracks.stream().map(Track::getId).collect(Collectors.toList())));
} else {
trackExportTotalCount = cursor.getCount(); exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.getId()))).collect(Collectors.toList()));
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();
}
} }
trackExportTotalCount = exportTasks.size();
} }
/** /**
* Enqueue track identified by UUID to be exported if not exported already or there is a conflict resolution. * 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) { private void export(ExportTask exportTask, ConflictResolutionStrategy conflictResolution) {
boolean fileExists = exportFileExists(track); boolean fileExists = exportFileExists(exportTask);
if (fileExists && conflictResolution == CONFLICT_NONE) { if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) {
conflict(track); conflict(exportTask);
} else if (fileExists && conflictResolution == CONFLICT_SKIP) { } else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) {
trackExportSkippedCount++; trackExportSkippedCount++;
onExportCompleted(track); nextExport(exportTask);
} else { } else {
ExportService.enqueue(this, resultReceiver, track.getId(), trackFileFormat, directoryUri); ExportService.enqueue(this, resultReceiver, exportTask, directoryUri);
} }
} }
private void export(Track track) { private void export(ExportTask exportTask) {
export(track, autoConflict); export(exportTask, autoConflict);
} }
private boolean exportFileExists(Track track) { @Deprecated //TODO Check should be done in ExportService
String filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); private boolean exportFileExists(ExportTask exportTask) {
String filename;
if (exportTask.isMultiExport()) {
filename = TrackFilenameGenerator.format(exportTask.getFilename(), exportTask.getTrackFileFormat());
} else {
Track track = contentProviderUtils.getTrack(exportTask.getTrackIds().get(0));
filename = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
}
return directoryFiles.stream().anyMatch(filename::equals); return directoryFiles.stream().anyMatch(filename::equals);
} }
@@ -269,31 +282,29 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
} }
private void setProgress() { private void setProgress() {
int done = getTotalDone(); viewBinding.exportProgressDone.setText("" + getTotalDone());
viewBinding.exportProgressDone.setText("" + done);
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount); viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f)); viewBinding.exportProgressBar.setProgress((int) ((float) getTotalDone() / (float) trackExportTotalCount * 100f));
viewBinding.exportProgressSummaryNew.setText(String.valueOf(trackExportSuccessCount)); viewBinding.exportProgressSummaryNew.setText("" + trackExportSuccessCount);
viewBinding.exportProgressSummaryOverwrite.setText(String.valueOf(trackExportOverwrittenCount)); viewBinding.exportProgressSummaryOverwrite.setText("" + trackExportOverwrittenCount);
viewBinding.exportProgressSummarySkip.setText(String.valueOf(trackExportSkippedCount)); viewBinding.exportProgressSummarySkip.setText("" + trackExportSkippedCount);
viewBinding.exportProgressSummaryErrors.setText(String.valueOf(trackExportErrorCount)); viewBinding.exportProgressSummaryErrors.setText("" + trackExportErrorCount);
viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryNewGroup.setVisibility(trackExportSuccessCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryOverwriteGroup.setVisibility(trackExportOverwrittenCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummarySkipGroup.setVisibility(trackExportSkippedCount > 0 ? View.VISIBLE : View.GONE);
viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE); viewBinding.exportProgressSummaryErrorsGroup.setVisibility(trackExportErrorCount > 0 ? View.VISIBLE : View.GONE);
} }
private void onExportCompleted(Track track) { private void nextExport(@Nullable ExportTask exportTask) {
tracks.remove(track); exportTasks.remove(exportTask);
setProgress(); setProgress();
if (tracks.isEmpty()) { if (exportTasks.isEmpty()) {
onExportEnded(); onExportEnded();
return; return;
} }
export(tracks.get(0)); export(exportTasks.get(0));
} }
private void onExportEnded() { private void onExportEnded() {
@@ -318,30 +329,32 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
} }
@Override @Override
public void onExportSuccess(Track.Id trackId) { public void onExportSuccess(ExportTask exportTask) {
Track track = contentProviderUtils.getTrack(trackId); if (exportFileExists(exportTask)) {
if (exportFileExists(track)) {
trackExportOverwrittenCount++; trackExportOverwrittenCount++;
} else { } else {
trackExportSuccessCount++; trackExportSuccessCount++;
} }
onExportCompleted(track); nextExport(exportTask);
} }
@Override @Override
public void onExportError(Track.Id trackId) { public void onExportError(ExportTask exportTask) {
Track track = contentProviderUtils.getTrack(trackId);
trackExportErrorCount++; 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) { private void conflict(ExportTask exportTask) {
PendingConflict newConflict = new PendingConflict(track); PendingConflict newConflict = new PendingConflict(exportTask);
conflictsQueue.add(newConflict); conflictsQueue.add(newConflict);
if (conflictsQueue.size() == 1) { if (conflictsQueue.size() == 1) {
@@ -353,10 +366,10 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* Handle conflicts (exporting file already exists). * Handle conflicts (exporting file already exists).
*/ */
private class PendingConflict { private class PendingConflict {
private final Track track; private final ExportTask exportTask;
public PendingConflict(Track track) { public PendingConflict(ExportTask exportTask) {
this.track = track; this.exportTask = exportTask;
} }
/** /**
@@ -366,14 +379,20 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
* @return true if it could resolve the conflict or false otherwise. * @return true if it could resolve the conflict or false otherwise.
*/ */
public boolean resolve() { 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)); 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); setConflictVisibility(View.VISIBLE);
return false; return false;
} }
export(track); export(exportTask);
return true; return true;
} }
@@ -381,10 +400,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. * Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
*/ */
public void overwrite() { public void overwrite() {
export(track, CONFLICT_OVERWRITE); export(exportTask, ConflictResolutionStrategy.CONFLICT_OVERWRITE);
if (viewBinding.exportProgressApplyToAll.isChecked()) { if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_OVERWRITE; autoConflict = ConflictResolutionStrategy.CONFLICT_OVERWRITE;
} }
} }
@@ -392,10 +411,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. * Skip the export file and set the autoConflict if user set the "do it for all" switch button.
*/ */
public void skip() { public void skip() {
export(track, CONFLICT_SKIP); export(exportTask, ConflictResolutionStrategy.CONFLICT_SKIP);
if (viewBinding.exportProgressApplyToAll.isChecked()) { if (viewBinding.exportProgressApplyToAll.isChecked()) {
autoConflict = CONFLICT_SKIP; autoConflict = ConflictResolutionStrategy.CONFLICT_SKIP;
} }
} }
} }
@@ -12,6 +12,9 @@ import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService; import androidx.core.app.JobIntentService;
import androidx.documentfile.provider.DocumentFile; 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.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat; 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 int JOB_ID = 1;
private static final String EXTRA_RECEIVER = "extra_receiver"; private static final String EXTRA_RECEIVER = "extra_receiver";
private static final String EXTRA_TRACK_ID = "extra_track_id"; private static final String EXTRA_EXPORT_TASK = "export_task";
private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format";
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri"; private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
private static final String TAG = ExportService.class.getSimpleName(); 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 intent = new Intent(context, JobService.class);
intent.putExtra(EXTRA_RECEIVER, receiver); intent.putExtra(EXTRA_RECEIVER, receiver);
intent.putExtra(EXTRA_TRACK_ID, trackId); intent.putExtra(EXTRA_EXPORT_TASK, exportTask);
intent.putExtra(EXTRA_TRACK_FILE_FORMAT, trackFileFormat);
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri); intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
enqueueWork(context, ExportService.class, JOB_ID, intent); enqueueWork(context, ExportService.class, JOB_ID, intent);
} }
@@ -40,13 +41,12 @@ public class ExportService extends JobIntentService {
protected void onHandleWork(@NonNull Intent intent) { protected void onHandleWork(@NonNull Intent intent) {
// Get all data. // Get all data.
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER); ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID); ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK);
TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT);
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI); Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
// Prepare resultCode and bundle to send to the receiver. // Prepare resultCode and bundle to send to the receiver.
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId); bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask);
// Build directory file. // Build directory file.
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri); DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
@@ -57,9 +57,7 @@ public class ExportService extends JobIntentService {
} }
// Export. // Export.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); boolean success = ExportUtils.exportTrack(this, directoryFile, exportTask);
Track track = contentProviderUtils.getTrack(trackId);
boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
// Send result to the receiver. // Send result to the receiver.
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR; int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
@@ -17,7 +17,7 @@ public class ExportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1; public static final int RESULT_CODE_SUCCESS = 1;
public static final int RESULT_CODE_ERROR = 0; 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; private final Receiver receiver;
@@ -28,19 +28,19 @@ public class ExportServiceResultReceiver extends ResultReceiver {
@Override @Override
protected void onReceiveResult(int resultCode, Bundle resultData) { 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) { switch (resultCode) {
case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(trackId); case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask);
case RESULT_CODE_ERROR -> receiver.onExportError(trackId); case RESULT_CODE_ERROR -> receiver.onExportError(exportTask);
default -> throw new RuntimeException("Unknown resultCode."); default -> throw new RuntimeException("Unknown resultCode.");
} }
} }
public interface Receiver { 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) {
} }
} }
} }
@@ -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<Track.Id> trackIds;
public ExportTask(@Nullable String filename, @NonNull TrackFileFormat trackFileFormat, @NonNull List<Track.Id> 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<Track.Id> 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<ExportTask> CREATOR = new Creator<>() {
@Override
public ExportTask createFromParcel(Parcel in) {
return new ExportTask(in);
}
@Override
public ExportTask[] newArray(int size) {
return new ExportTask[size];
}
};
}
@@ -98,12 +98,7 @@ public class GPXTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try { try {
prepare(outputStream); prepare(outputStream);
writeHeader(); writeHeader();
@@ -99,18 +99,14 @@ public class KMLTrackExporter implements TrackExporter {
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
} }
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try { try {
prepare(outputStream); prepare(outputStream);
writeHeader(tracks); writeHeader(tracks);
for (Track track : tracks) { for (Track track : tracks) {
writeMarkers(track); writeMarkers(track);
} }
boolean hasMultipleTracks = tracks.length > 1; boolean hasMultipleTracks = tracks.size() > 1;
if (hasMultipleTracks) { if (hasMultipleTracks) {
writeMultiTrackBegin(); writeMultiTrackBegin();
} }
@@ -222,7 +218,7 @@ public class KMLTrackExporter implements TrackExporter {
} }
} }
private void writeHeader(Track[] tracks) { private void writeHeader(List<Track> tracks) {
if (printWriter != null) { if (printWriter != null) {
printWriter.println( printWriter.println(
""" """
@@ -241,7 +237,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<open>1</open>"); printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>"); printWriter.println("<visibility>1</visibility>");
Track track = tracks[0]; Track track = tracks.get(0);
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:generator>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:generator>"); printWriter.println("<atom:generator>" + StringUtils.formatCData(context.getString(R.string.app_name)) + "</atom:generator>");
@@ -293,7 +289,7 @@ public class KMLTrackExporter implements TrackExporter {
private void writeMultiTrackBegin() { private void writeMultiTrackBegin() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<Folder id=tour>"); printWriter.println("<Folder id=\"tracks\">");
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>"); printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>"); printWriter.println("<open>1</open>");
} }
@@ -28,6 +28,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -64,12 +65,7 @@ public class KmzTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) { public boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream) {
return writeTrack(new Track[]{track}, outputStream);
}
@Override
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
// Add kml file // Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE); ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
@@ -91,7 +87,7 @@ public class KmzTrackExporter implements TrackExporter {
} }
} }
private void addImages(Context context, Track[] tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException { private void addImages(Context context, List<Track> tracks, ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
for (Track track : tracks) { for (Track track : tracks) {
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) { try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, -1)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.exporter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
@@ -29,7 +30,5 @@ import de.dennisguse.opentracks.data.models.Track;
*/ */
public interface TrackExporter { public interface TrackExporter {
boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream); boolean writeTrack(List<Track> tracks, @NonNull OutputStream outputStream);
boolean writeTrack(Track track, @NonNull OutputStream outputStream);
} }
@@ -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 { public static class DefaultTrackExportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override @Override
@@ -17,13 +17,16 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat; 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.ExportService;
import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver; 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.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -44,7 +47,7 @@ public class ExportUtils {
ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() { ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() {
@Override @Override
public void onExportError(Track.Id trackId) { public void onExportError(ExportTask unused) {
Intent intent = new Intent(context, SettingsActivity.class); Intent intent = new Intent(context, SettingsActivity.class);
intent.putExtra(SettingsActivity.EXTRAS_CHECK_EXPORT_DIRECTORY, true); intent.putExtra(SettingsActivity.EXTRAS_CHECK_EXPORT_DIRECTORY, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -52,21 +55,29 @@ 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) { public static boolean exportTrack(Context context, DocumentFile directory, ExportTask exportTask) {
TrackExporter trackExporter = trackFileFormat.createTrackExporter(context, new ContentProviderUtils(context)); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Track> 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 {
String filename = TrackFilenameGenerator.format(exportTask.getFilename(), exportTask.getTrackFileFormat());
exportDocumentFileUri = getExportDocumentFileUri(context, filename, exportTask.getTrackFileFormat(), directory);
}
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track, trackFileFormat, directory);
if (exportDocumentFileUri == null) { if (exportDocumentFileUri == null) {
Log.e(TAG, "Couldn't create document file for export"); Log.e(TAG, "Couldn't create document file for export");
return false; return false;
} }
TrackExporter trackExporter = exportTask.getTrackFileFormat().createTrackExporter(context, contentProviderUtils);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) { try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
if (trackExporter.writeTrack(track, outputStream)) { if (trackExporter.writeTrack(tracks, outputStream)) {
return true; return true;
} else { } else {
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) { if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
@@ -102,6 +113,10 @@ public class ExportUtils {
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) { private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
String exportFileName = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat); 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); Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
if (exportDocumentFileUri == null) { if (exportDocumentFileUri == null) {
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName); final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
+7 -6
View File
@@ -210,12 +210,12 @@ limitations under the License.
<string name="export_cannot_write_to_dir">Cannot write to export directory.</string> <string name="export_cannot_write_to_dir">Cannot write to export directory.</string>
<string name="export_dir_not_writable">" (not writable!)"</string> <string name="export_dir_not_writable">" (not writable!)"</string>
<string name="export_progress_message">Exporting to %1$s&#8230;</string> <string name="export_progress_message">Exporting to %1$s&#8230;</string>
<string name="export_track_already_exists_msg">The track %1$s already exists at the destination directory.</string> <string name="export_track_already_exists_msg">The file %1$s already exists at the destination directory.</string>
<string name="export_all_do_it_for_all">do it for all conflicts</string> <string name="export_all_do_it_for_all">do it for all conflicts</string>
<string name="export_track_errors">Tracks not exported</string> <string name="export_track_errors">Files not exported</string>
<string name="export_progress_summary_new_msg">New tracks exported</string> <string name="export_progress_summary_new_msg">New files exported</string>
<string name="export_progress_summary_overwrite_msg">Tracks overwritten</string> <string name="export_progress_summary_overwrite_msg">Files overwritten</string>
<string name="export_progress_summary_skip_msg">Tracks skipped</string> <string name="export_progress_summary_skip_msg">Files skipped</string>
<string name="export_progress_summary_errors_msg">Errors</string> <string name="export_progress_summary_errors_msg">Errors</string>
<!-- External Storage --> <!-- External Storage -->
<!-- Generic --> <!-- Generic -->
@@ -297,7 +297,8 @@ limitations under the License.
<string name="menu_aggregated_statistics">Aggregated stats</string> <string name="menu_aggregated_statistics">Aggregated stats</string>
<string name="menu_delete">Delete</string> <string name="menu_delete">Delete</string>
<string name="menu_edit">Edit</string> <string name="menu_edit">Edit</string>
<string name="menu_export_all">Export all</string> <string name="menu_export_all">Export all (one file per track)</string>
<string name="menu_export_all_one_file">Export all (one file for all tracks)</string>
<string name="menu_help">Help</string> <string name="menu_help">Help</string>
<string name="menu_import_all">Import all</string> <string name="menu_import_all">Import all</string>
<string name="menu_insert_marker">Insert marker</string> <string name="menu_insert_marker">Insert marker</string>
+7 -1
View File
@@ -26,6 +26,13 @@
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity"
android:targetPackage="@string/applicationId" /> android:targetPackage="@string/applicationId" />
</Preference> </Preference>
<Preference
android:key="@string/settings_export"
android:title="@string/menu_export_all_one_file">
<intent
android:targetClass="de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity"
android:targetPackage="@string/applicationId" />
</Preference>
<Preference <Preference
android:key="@string/settings_default_export_directory_key" android:key="@string/settings_default_export_directory_key"
android:title="@string/settings_default_export_uri_title"> android:title="@string/settings_default_export_uri_title">
@@ -48,5 +55,4 @@
android:title="@string/settings_export_filename_title" android:title="@string/settings_export_filename_title"
android:defaultValue="@string/export_filename_format_default" /> android:defaultValue="@string/export_filename_format_default" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>