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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +