diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 81d7cf1ea..72795b89a 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -154,9 +154,7 @@ limitations under the License.
-
+
@@ -196,5 +194,10 @@ limitations under the License.
android:foregroundServiceType="location|connectedDevice"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
+
+
diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ExportFinishedDialogFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ExportFinishedDialogFragment.java
deleted file mode 100644
index 3eb8feb35..000000000
--- a/src/main/java/de/dennisguse/opentracks/fragments/ExportFinishedDialogFragment.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2012 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package de.dennisguse.opentracks.fragments;
-
-import android.app.Dialog;
-import android.content.Context;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentManager;
-
-import de.dennisguse.opentracks.R;
-
-public class ExportFinishedDialogFragment extends DialogFragment {
-
- private static final String TAG = ExportFinishedDialogFragment.class.getSimpleName();
-
- private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
- private static final String EXTRA_EXPORTED_TRACK_COUNT = "exported_track_count";
- private static final String EXTRA_TOTAL_TRACK_COUNT = "";
-
- private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
-
- private DismissCallback caller;
-
- public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
- Bundle bundle = new Bundle();
- bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
- bundle.putInt(EXTRA_EXPORTED_TRACK_COUNT, successCount);
- bundle.putInt(EXTRA_TOTAL_TRACK_COUNT, fileCount);
-
- ExportFinishedDialogFragment dialogFragment = new ExportFinishedDialogFragment();
- dialogFragment.setArguments(bundle);
- dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
- }
-
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
- try {
- caller = (DismissCallback) context;
- } catch (ClassCastException e) {
- throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
- }
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
- int exportedTracksCount = getArguments().getInt(EXTRA_EXPORTED_TRACK_COUNT);
- int totalTracksCount = getArguments().getInt(EXTRA_TOTAL_TRACK_COUNT);
-
- int iconId;
- int titleId;
- String message;
- String totalTracks = getResources().getQuantityString(R.plurals.tracks, totalTracksCount, totalTracksCount);
- if (exportedTracksCount == totalTracksCount) {
- iconId = R.drawable.ic_dialog_success_24dp;
- titleId = R.string.generic_success_title;
- message = getString(
- R.string.export_success, totalTracks, directoryDisplayName);
- } else {
- iconId = R.drawable.ic_dialog_error_24dp;
- titleId = R.string.generic_error_title;
- message = getString(R.string.export_error, exportedTracksCount, totalTracks, directoryDisplayName);
- }
- Dialog alertDialog = new AlertDialog.Builder(getContext()).setCancelable(true)
- .setIcon(iconId).setMessage(message)
- .setPositiveButton(R.string.generic_ok, (dialog, arg1) -> {
- dialog.dismiss();
- caller.onDismissed();
- }).setTitle(titleId)
- .create();
-
- alertDialog.setCanceledOnTouchOutside(false);
- return alertDialog;
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- caller = null;
- }
-
- public interface DismissCallback {
- void onDismissed();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ExportProgressDialogFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ExportProgressDialogFragment.java
deleted file mode 100644
index 205979989..000000000
--- a/src/main/java/de/dennisguse/opentracks/fragments/ExportProgressDialogFragment.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright 2012 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package de.dennisguse.opentracks.fragments;
-
-import android.app.Dialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.documentfile.provider.DocumentFile;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentManager;
-
-import de.dennisguse.opentracks.R;
-import de.dennisguse.opentracks.content.data.Track;
-import de.dennisguse.opentracks.content.data.TracksColumns;
-import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
-import de.dennisguse.opentracks.io.file.TrackFileFormat;
-import de.dennisguse.opentracks.util.DialogUtils;
-import de.dennisguse.opentracks.util.ExportUtils;
-import de.dennisguse.opentracks.util.FileUtils;
-
-public class ExportProgressDialogFragment extends DialogFragment {
-
- private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
- private static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
-
- private static final String TAG = ExportProgressDialogFragment.class.getSimpleName();
-
- public static final String EXPORT_DIALOG_TAG = "export_dialog_tag";
-
- private DismissCallback caller;
-
- private ExportThread exportThread;
-
- private String directoryDisplayName;
-
- private int trackExportSuccessCount;
- private int trackCount;
-
- /**
- * Create a new instance.
- */
- public static void showDialog(FragmentManager fragmentManager, TrackFileFormat trackFileFormat, Uri documentFileUri) {
- Bundle bundle = new Bundle();
- bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
- bundle.putSerializable(EXTRA_TRACKFILEFORMAT_KEY, trackFileFormat);
-
- ExportProgressDialogFragment dialogFragment = new ExportProgressDialogFragment();
- dialogFragment.setArguments(bundle);
- dialogFragment.setRetainInstance(true);
- dialogFragment.show(fragmentManager, EXPORT_DIALOG_TAG);
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // This fragment uses a background thread, so it should not be recreated during activity re-creation.
- setRetainInstance(true);
-
- Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
- TrackFileFormat trackFileFormat = (TrackFileFormat) getArguments().getSerializable(EXTRA_TRACKFILEFORMAT_KEY);
- DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
-
- directoryDisplayName = FileUtils.getPath(documentFile);
-
- exportThread = new ExportThread(trackFileFormat, documentFile);
- }
-
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
- try {
- caller = (DismissCallback) context;
- } catch (ClassCastException e) {
- throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
- }
- }
-
- @Override
- @NonNull
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(getContext(),
- R.string.export_progress_message,
- dialog -> {
- exportThread.interrupt();
- dialog.dismiss();
- caller.onExportCanceled(directoryDisplayName, trackExportSuccessCount, trackCount);
- }, directoryDisplayName);
-
- progressDialog.setIndeterminate(false);
- progressDialog.setMax(trackCount);
- progressDialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
-
- return progressDialog;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- setProgress();
- if (!exportThread.isAlive()) {
- exportThread.start();
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (!exportThread.isAlive()) {
- exportThread.interrupt();
- }
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- caller = null;
- }
-
- private void onProgress(int trackImportSuccessCount, int fileCount) {
- this.trackExportSuccessCount = trackImportSuccessCount;
- this.trackCount = fileCount;
- setProgress();
- }
-
- private void setProgress() {
- ProgressDialog dialog = (ProgressDialog) getDialog();
- if (dialog != null) {
- dialog.setIndeterminate(false);
- dialog.setMax(trackCount);
- dialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
- }
- }
-
- private void onExportCompleted(int successCount, int fileCount) {
- if (caller != null && !isDetached()) {
- caller.onExportFinished(directoryDisplayName, successCount, fileCount);
- }
- dismissAllowingStateLoss();
- }
-
- private class ExportThread extends Thread {
-
- private final TrackFileFormat trackFileFormat;
-
- private final DocumentFile directory;
-
- ExportThread(TrackFileFormat trackFileFormat, DocumentFile directory) {
- this.directory = directory;
- this.trackFileFormat = trackFileFormat;
- }
-
- @Override
- public void run() {
- Context context = ExportProgressDialogFragment.this.getContext();
-
- ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
-
- try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
- if (cursor == null) {
- onExportCompleted(0, 0);
- return;
- }
-
- trackCount = cursor.getCount();
- for (int i = 0; i < trackCount; i++) {
- onProgress(i, trackCount);
- if (Thread.interrupted()) {
- return;
- }
-
- cursor.moveToPosition(i);
- Track track = contentProviderUtils.createTrack(cursor);
- if (track != null && ExportUtils.exportTrack(context, trackFileFormat, directory, track)) {
- trackExportSuccessCount++;
- }
- }
-
- onExportCompleted(trackExportSuccessCount, trackCount);
- }
- }
- }
-
- public interface DismissCallback {
- void onExportCanceled(String directoryDisplayName, int successCount, int fileCount);
-
- void onExportFinished(String directoryDisplayName, int successCount, int fileCount);
- }
-}
\ No newline at end of file
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 e8e143414..5c346f884 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,58 +16,419 @@
package de.dennisguse.opentracks.io.file.exporter;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+
+import androidx.documentfile.provider.DocumentFile;
+import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
-import de.dennisguse.opentracks.fragments.ExportFinishedDialogFragment;
-import de.dennisguse.opentracks.fragments.ExportProgressDialogFragment;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import de.dennisguse.opentracks.R;
+import de.dennisguse.opentracks.content.data.Track;
+import de.dennisguse.opentracks.content.data.TracksColumns;
+import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
+import de.dennisguse.opentracks.util.ExportUtils;
+import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity for saving tracks to the external storage.
*
* @author Rodrigo Damazio
*/
-public class ExportActivity extends FragmentActivity implements ExportProgressDialogFragment.DismissCallback, ExportFinishedDialogFragment.DismissCallback {
+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_TRACKFILEFORMAT_KEY = "trackfileformat";
- private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
+ private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict";
+ private static final String BUNDLE_SUCCESS_COUNT = "track_export_success_count";
+ private static final String BUNDLE_ERROR_COUNT = "track_export_error_ount";
+ private static final String BUNDLE_OVERWRITTEN_COUNT = "track_export_overwritten_count";
+ private static final String BUNDLE_SKIPPED_COUNT = "track_export_skipped_count";
+ private static final String BUNDLE_TOTAL_COUNT = "track_export_total_count";
+ private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files";
+
+ private static final int CONFLICT_NONE = 0;
+ private static final int CONFLICT_OVERWRITE = 1;
+ private static final int CONFLICT_SKIP = 2;
+
+ private TrackFileFormat trackFileFormat;
+ private Uri directoryUri;
+
+ private ExportServiceResultReceiver resultReceiver;
+
+ private String directoryDisplayName;
+ private List directoryFiles;
+
+ private int trackExportSuccessCount;
+ private int trackExportErrorCount;
+ private int trackExportOverwrittenCount;
+ private int trackExportSkippedCount;
+ private int trackExportTotalCount;
+
+ boolean doubleBackToCancel = false;
+
+ private TextView viewTotal;
+ private TextView viewDone;
+ private TextView viewSummary;
+ private ProgressBar viewProgressBar;
+ private ImageView viewAlertIcon;
+ private TextView viewAlertMsg;
+ private CheckBox viewDoItForAllSwitch;
+ private Button viewLeftButton;
+ private Button viewRightButton;
+
+ private int autoConflict;
+
+ private ContentProviderUtils contentProviderUtils;
+
+ // List of tracks to be exported.
+ private ArrayList trackIds = new ArrayList<>();
+
+ private LinkedBlockingQueue conflictsQueue = new LinkedBlockingQueue<>();
+ private Handler conflictsHandler = new Handler();
+
+ 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;
+ }
+
+ viewLeftButton.setOnClickListener((view) -> {
+ setConflictVisibility(View.GONE);
+ conflict.skip();
+ conflictsQueue.remove(conflict);
+ if (!conflictsQueue.isEmpty()) {
+ conflictsHandler.post(conflictsRunnable);
+ }
+ });
+
+ viewRightButton.setOnClickListener((view) -> {
+ setConflictVisibility(View.GONE);
+ conflict.overwrite();
+ conflictsQueue.remove(conflict);
+ if (!conflictsQueue.isEmpty()) {
+ conflictsHandler.post(conflictsRunnable);
+ }
+ });
+ }
+ }
+ };
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.export_progress_activity);
- if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
- Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
- TrackFileFormat trackfileformat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
- ExportProgressDialogFragment.showDialog(getSupportFragmentManager(), trackfileformat, directoryUri);
+ directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
+ trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
+
+ contentProviderUtils = new ContentProviderUtils(this);
+
+ DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
+ directoryDisplayName = FileUtils.getPath(documentFile);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
+
+ resultReceiver = new ExportServiceResultReceiver(new Handler());
+ resultReceiver.setReceiver(this);
+
+ viewTotal = findViewById(R.id.export_progress_total);
+ viewDone = findViewById(R.id.export_progress_done);
+ viewSummary = findViewById(R.id.export_progress_summary);
+ viewProgressBar = findViewById(R.id.export_progress_bar);
+ viewAlertIcon = findViewById(R.id.export_progress_alert_icon);
+ viewAlertMsg = findViewById(R.id.export_progress_alert_msg);
+ viewDoItForAllSwitch = findViewById(R.id.export_progress_toggle);
+ viewLeftButton = findViewById(R.id.export_progress_left_button);
+ viewRightButton = findViewById(R.id.export_progress_right_button);
+
+ if (savedInstanceState == null) {
+ autoConflict = CONFLICT_NONE;
+ setProgress();
+ new Thread(() -> {
+ directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
+ runOnUiThread(() -> initExport(0));
+ }).start();
+ } else {
+ autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT);
+ trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT);
+ trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT);
+ trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT);
+ trackExportSkippedCount = savedInstanceState.getInt(BUNDLE_SKIPPED_COUNT);
+ trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
+ directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
+
+ setProgress();
+ initExport(getTotalDone());
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
+ outState.putInt(BUNDLE_AUTO_CONFLICT, autoConflict);
+ outState.putInt(BUNDLE_SUCCESS_COUNT, trackExportSuccessCount);
+ outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount);
+ outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount);
+ outState.putInt(BUNDLE_SKIPPED_COUNT, trackExportSkippedCount);
+ outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
+ outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList) directoryFiles);
}
@Override
- public void onExportCanceled(String directoryDisplayName, int successCount, int fileCount) {
- onExportFinished(directoryDisplayName, successCount, fileCount);
+ protected void onDestroy() {
+ super.onDestroy();
+ conflictsQueue.clear();
+ trackIds.clear();
}
@Override
- public void onExportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
- runOnUiThread(() -> ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
+ public void onBackPressed() {
+ if (doubleBackToCancel || getTotalDone() == trackExportTotalCount) {
+ super.onBackPressed();
+ return;
+ }
+
+ this.doubleBackToCancel = true;
+ Toast.makeText(this, getString(R.string.export_click_twice_cancel), Toast.LENGTH_SHORT).show();
+
+ 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();
+ viewTotal.setText("" + trackExportTotalCount);
+ for (int i = from; i < trackExportTotalCount; i++) {
+ cursor.moveToPosition(i);
+ Track track = contentProviderUtils.createTrack(cursor);
+ trackIds.add(track.getId());
+ }
+
+ if (!trackIds.isEmpty()) {
+ export(trackIds.get(0));
+ } else {
+ onExportEnded();
+ }
+ }
+ }
+
+ /**
+ * Enqueue track identified by trackId to be exported if not exported already or there is a conflict resolution.
+ *
+ * @param trackId the track's id.
+ * @param conflictResolution conflict resolution to be applied if needed.
+ */
+ private void export(Track.Id trackId, int conflictResolution) {
+ boolean fileExists = ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles);
+
+ if (fileExists && conflictResolution == CONFLICT_NONE) {
+ conflict(trackId);
+ } else if (fileExists && conflictResolution == CONFLICT_SKIP) {
+ trackExportSkippedCount++;
+ onExportCompleted(trackId);
+ } else {
+ ExportService.enqueue(this, resultReceiver, trackId, trackFileFormat, directoryUri);
+ }
+ }
+
+ private void export(Track.Id trackId) {
+ export(trackId, autoConflict);
+ }
+
+ private void setConflictVisibility(int visibility) {
+ viewAlertIcon.setVisibility(visibility);
+ viewAlertMsg.setVisibility(visibility);
+ viewDoItForAllSwitch.setVisibility(visibility);
+ viewLeftButton.setVisibility(visibility);
+ viewRightButton.setVisibility(visibility);
+ }
+
+ private int getTotalDone() {
+ return trackExportSuccessCount + trackExportOverwrittenCount + trackExportSkippedCount + trackExportErrorCount;
+ }
+
+ private void setProgress() {
+ int done = getTotalDone();
+
+ viewDone.setText("" + done);
+ viewTotal.setText("" + trackExportTotalCount);
+
+ viewProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f));
+ viewSummary.setText(getString(R.string.export_progress_review, getTotalDone(), trackExportSuccessCount, trackExportOverwrittenCount, trackExportSkippedCount, trackExportErrorCount));
+ }
+
+ private void onExportCompleted(Track.Id trackId) {
+ trackIds.remove(trackId);
+ setProgress();
+ if (trackIds.isEmpty()) {
+ onExportEnded();
+ return;
+ }
+ export(trackIds.get(0));
+ }
+
+ private void onExportEnded() {
+ viewAlertIcon.setVisibility(View.VISIBLE);
+ viewAlertMsg.setVisibility(View.VISIBLE);
+ viewRightButton.setVisibility(View.VISIBLE);
+ viewRightButton.setText(getString(R.string.generic_ok));
+
+ if (trackExportErrorCount > 0) {
+ viewLeftButton.setVisibility(View.VISIBLE);
+ viewLeftButton.setText(getString(R.string.export_show_errors));
+ viewLeftButton.setOnClickListener((view) -> new ErrorListDialog().show(getSupportFragmentManager(), ErrorListDialog.TAG));
+ viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
+ String msg = getResources().getQuantityString(R.plurals.export_all_end_with_errors, trackExportErrorCount, trackExportErrorCount);
+ viewAlertMsg.setText(msg);
+ } else {
+ viewLeftButton.setVisibility(View.GONE);
+ viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_dialog_success_24dp));
+ viewAlertMsg.setText(getString(R.string.export_all_end));
+ }
+
+ viewRightButton.setOnClickListener((view) -> finish());
}
@Override
- public void onDismissed() {
- finish();
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultData == null) {
+ throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
+ }
+
+ Track.Id trackId = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID);
+
+ switch (resultCode) {
+ case ExportServiceResultReceiver.RESULT_CODE_ERROR:
+ trackExportErrorCount++;
+ Track track = contentProviderUtils.getTrack(trackId);
+ ErrorListDialog.trackErrors.add(track.getName());
+ break;
+ case ExportServiceResultReceiver.RESULT_CODE_SUCCESS:
+ if (ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles)) {
+ trackExportOverwrittenCount++;
+ } else {
+ trackExportSuccessCount++;
+ }
+ break;
+ default:
+ throw new RuntimeException(TAG + ": export service result code invalid: " + resultCode);
+ }
+
+ onExportCompleted(trackId);
+ }
+
+ private void conflict(Track.Id trackId) {
+ PendingConflict newConflict = new PendingConflict(trackId);
+ conflictsQueue.add(newConflict);
+
+ if (conflictsQueue.size() == 1) {
+ conflictsHandler.post(conflictsRunnable);
+ }
+ }
+
+ /**
+ * Handle conflicts (exporting file already exists).
+ */
+ private class PendingConflict {
+ private Track.Id trackId;
+
+ public PendingConflict(Track.Id trackId) {
+ this.trackId = trackId;
+ }
+
+ /**
+ * Try to resolve the conflict if user gave the info needed.
+ * Otherwise shows the buttons and views needed to gives the user the possibility of take a decision.
+ *
+ * @return true if it could resolve the conflict or false otherwise.
+ */
+ public boolean resolve() {
+ if (autoConflict == CONFLICT_NONE) {
+ Track track = contentProviderUtils.getTrack(trackId);
+ viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
+ viewAlertMsg.setText(getString(R.string.export_track_already_exists_msg, track.getName()));
+ setConflictVisibility(View.VISIBLE);
+ return false;
+ }
+
+ export(trackId);
+ return true;
+ }
+
+ /**
+ * Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
+ */
+ public void overwrite() {
+ export(trackId, CONFLICT_OVERWRITE);
+
+ if (viewDoItForAllSwitch.isChecked()) {
+ autoConflict = CONFLICT_OVERWRITE;
+ }
+ }
+
+ /**
+ * Skip the export file and set the autoConflict if user set the "do it for all" switch button.
+ */
+ public void skip() {
+ export(trackId, CONFLICT_SKIP);
+
+ if (viewDoItForAllSwitch.isChecked()) {
+ autoConflict = CONFLICT_SKIP;
+ }
+ }
+ }
+
+ public static class ErrorListDialog extends DialogFragment {
+
+ public static final String TAG = ErrorListDialog.class.getSimpleName();
+
+ public static List trackErrors = new ArrayList<>();
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final String[] tracks = trackErrors.stream().toArray(String[]::new);
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.export_track_errors))
+ .setItems(tracks, null)
+ .setPositiveButton(R.string.generic_ok, (dialog, which) -> dismiss());
+ AlertDialog dialog = alertDialogBuilder.create();
+ return dialog;
+ }
}
}
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
new file mode 100644
index 000000000..54a7a122f
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java
@@ -0,0 +1,61 @@
+package de.dennisguse.opentracks.io.file.exporter;
+
+import android.app.job.JobService;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.JobIntentService;
+import androidx.documentfile.provider.DocumentFile;
+
+import de.dennisguse.opentracks.content.data.Track;
+import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
+import de.dennisguse.opentracks.io.file.TrackFileFormat;
+import de.dennisguse.opentracks.util.ExportUtils;
+
+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_DIRECTORY_URI = "extra_directory_uri";
+
+ public static void enqueue(Context context, ExportServiceResultReceiver receiver, Track.Id trackId, TrackFileFormat trackFileFormat, 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_DIRECTORY_URI, directoryUri);
+ enqueueWork(context, ExportService.class, JOB_ID, intent);
+ }
+
+ @Override
+ 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);
+ Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
+
+ // Build directory file.
+ DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
+
+ // Export.
+ ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
+ Track track = contentProviderUtils.getTrack(trackId);
+ boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
+
+ // Prepare resultCode and bundle to send to the receiver.
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
+
+ // Send result to the receiver.
+ int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
+ resultReceiver.send(resultCode, bundle);
+ }
+}
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
new file mode 100644
index 000000000..cea13a887
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportServiceResultReceiver.java
@@ -0,0 +1,38 @@
+package de.dennisguse.opentracks.io.file.exporter;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+
+/**
+ * Create a new ResultReceive to receive results.
+ * Your {@link #onReceiveResult} method will be called from the thread running handler if given, or from an arbitrary thread if null.
+ */
+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";
+
+ private Receiver receiver;
+
+ public ExportServiceResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (receiver != null) {
+ receiver.onReceiveResult(resultCode, resultData);
+ }
+ }
+
+ public void setReceiver(Receiver receiver) {
+ this.receiver = receiver;
+ }
+
+ public interface Receiver {
+ void onReceiveResult(int resultCode, Bundle resultData);
+ }
+}
diff --git a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java
index 2e1ab83fc..bca6f2152 100644
--- a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java
@@ -1,6 +1,10 @@
package de.dennisguse.opentracks.util;
+import android.content.ContentResolver;
import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
@@ -8,6 +12,8 @@ import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -26,23 +32,44 @@ public class ExportUtils {
}
}
+ public static List getAllFiles(Context context, Uri directoryUri) {
+ List fileNames = new ArrayList<>();
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
+
+ try {
+ Cursor c = resolver.query(
+ childrenUri,
+ new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME },
+ null,null, null
+ );
+ while (c.moveToNext()) {
+ fileNames.add(c.getString(0));
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed query: " + e);
+ }
+
+ return fileNames;
+ }
+
public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) {
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
- DocumentFile exportDocumentFile = ExportUtils.getExportDocumentFile(track.getId(), trackFileFormat.getExtension(), directory, trackFileFormat.getMimeType());
+ Uri exportDocumentFileUri = getExportDocumentFileUri(context, track.getId(), trackFileFormat, directory);
- try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFile.getUri())) {
+ try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
if (trackExporter.writeTrack(track, outputStream)) {
return true;
} else {
- if (!exportDocumentFile.delete()) {
+ if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
Log.e(TAG, "Unable to delete exportDocumentFile");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
- Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFile.getName(), e);
+ Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFileUri, e);
return false;
} catch (IOException e) {
Log.e(TAG, "Unable to close exportDocumentFile output stream", e);
@@ -50,16 +77,44 @@ public class ExportUtils {
}
}
- public static DocumentFile getExportDocumentFile(Track.Id trackId, String trackFileFormatExtension, DocumentFile directory, String mimeType) {
- String fileName = getExportFileNameByTrackId(trackId, trackFileFormatExtension);
- DocumentFile file = directory.findFile(fileName);
- if (file == null) {
- file = directory.createFile(mimeType, fileName);
+ public static boolean isExportFileExists(Track.Id trackId, String trackFileFormatExtension, List filesName) {
+ return filesName.contains(getExportFileNameByTrackId(trackId, trackFileFormatExtension));
+ }
+
+ private static Uri getExportDocumentFileUri(Context context, Track.Id trackId, TrackFileFormat trackFileFormat, DocumentFile directory) {
+ String exportFileName = getExportFileNameByTrackId(trackId, trackFileFormat.getExtension());
+ Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
+ if (exportDocumentFileUri == null) {
+ exportDocumentFileUri = directory.createFile(trackFileFormat.getMimeType(), exportFileName).getUri();
}
- return file;
+ return exportDocumentFileUri;
}
private static String getExportFileNameByTrackId(Track.Id trackId, String trackFileFormatExtension) {
return trackId.getId() + "." + trackFileFormatExtension;
}
+
+ private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
+
+ try {
+ Cursor c = resolver.query(
+ childrenUri,
+ new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME },
+ null,null, null
+ );
+ while (c.moveToNext()) {
+ final String documentId = c.getString(0);
+ final String documentName = c.getString(1);
+ if (documentName.equals(exportFileName)) {
+ return DocumentsContract.buildDocumentUriUsingTree(directoryUri, documentId);
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Find file error: failed query: " + e);
+ }
+
+ return null;
+ }
}
diff --git a/src/main/res/drawable/ic_report_problem_24.xml b/src/main/res/drawable/ic_report_problem_24.xml
new file mode 100644
index 000000000..a02bc138c
--- /dev/null
+++ b/src/main/res/drawable/ic_report_problem_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/res/layout/export_progress_activity.xml b/src/main/res/layout/export_progress_activity.xml
new file mode 100644
index 000000000..cbed65770
--- /dev/null
+++ b/src/main/res/layout/export_progress_activity.xml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 0534380ce..3db46bc32 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -216,6 +216,23 @@ limitations under the License.
Exporting to %1$s…
Exported %1$s to %2$s
Export to…
+ The track %1$s already exists at the destination directory.
+
+ - Completed with one error
+ - Completed with %1$d errors
+
+ Completed
+ do it for all conflicts
+ Click back again to cancel
+ Show Errors
+ Tracks not exported
+
+ Total exported tracks: %1$d\n
+ - New tracks exported: %2$d\n
+ - Tracks overwritten: %3$d\n
+ - Tracks skipped: %4$d\n
+ - Errors: %5$d
+
%1$d files
@@ -243,6 +260,9 @@ limitations under the License.
Success
Tracks
Yes
+ Overwrite
+ Skip
+ /
‘Location access’
GPS not enabled. Tap here to go to %1$s. Press the back button to return.
diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml
index 5b6b1e5d2..af21de4e1 100644
--- a/src/main/res/values/styles.xml
+++ b/src/main/res/values/styles.xml
@@ -224,4 +224,6 @@ limitations under the License.
- wrap_content
- wrap_content
+
+