forked from upstream-mirrors/OpenTracks
Export All: ask to the user when conflicts (track already exported in the selected directory) and show a summary. Fixes #451.
This commit is contained in:
@@ -154,9 +154,7 @@ limitations under the License.
|
|||||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
|
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
|
||||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
|
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".io.file.exporter.ExportActivity" />
|
||||||
android:name=".io.file.exporter.ExportActivity"
|
|
||||||
android:theme="@style/ThemeCustomTransparentActivity" />
|
|
||||||
|
|
||||||
<activity android:name=".settings.SettingsActivity" />
|
<activity android:name=".settings.SettingsActivity" />
|
||||||
|
|
||||||
@@ -196,5 +194,10 @@ limitations under the License.
|
|||||||
android:foregroundServiceType="location|connectedDevice"
|
android:foregroundServiceType="location|connectedDevice"
|
||||||
android:icon="@drawable/ic_logo_color_24dp"
|
android:icon="@drawable/ic_logo_color_24dp"
|
||||||
android:label="@string/recording_service" />
|
android:label="@string/recording_service" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".io.file.exporter.ExportService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,58 +16,419 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks.io.file.exporter;
|
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.net.Uri;
|
||||||
import android.os.Bundle;
|
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.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.fragments.ExportFinishedDialogFragment;
|
import java.util.ArrayList;
|
||||||
import de.dennisguse.opentracks.fragments.ExportProgressDialogFragment;
|
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.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.util.ExportUtils;
|
||||||
|
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
|
||||||
*/
|
*/
|
||||||
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_DIRECTORY_URI_KEY = "directory_uri";
|
||||||
|
|
||||||
public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
|
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<String> 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<Track.Id> trackIds = new ArrayList<>();
|
||||||
|
|
||||||
|
private LinkedBlockingQueue<PendingConflict> 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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.export_progress_activity);
|
||||||
|
|
||||||
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
|
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
||||||
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
|
||||||
TrackFileFormat trackfileformat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
|
|
||||||
ExportProgressDialogFragment.showDialog(getSupportFragmentManager(), trackfileformat, directoryUri);
|
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
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(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<String>) directoryFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExportCanceled(String directoryDisplayName, int successCount, int fileCount) {
|
protected void onDestroy() {
|
||||||
onExportFinished(directoryDisplayName, successCount, fileCount);
|
super.onDestroy();
|
||||||
|
conflictsQueue.clear();
|
||||||
|
trackIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
|
public void onBackPressed() {
|
||||||
runOnUiThread(() -> ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
|
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
|
@Override
|
||||||
public void onDismissed() {
|
public void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
finish();
|
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<String> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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 <var>handler</var> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
package de.dennisguse.opentracks.util;
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
@@ -8,6 +12,8 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
@@ -26,23 +32,44 @@ public class ExportUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<String> getAllFiles(Context context, Uri directoryUri) {
|
||||||
|
List<String> 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) {
|
public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) {
|
||||||
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
|
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)) {
|
if (trackExporter.writeTrack(track, outputStream)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (!exportDocumentFile.delete()) {
|
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
|
||||||
Log.e(TAG, "Unable to delete exportDocumentFile");
|
Log.e(TAG, "Unable to delete exportDocumentFile");
|
||||||
}
|
}
|
||||||
Log.e(TAG, "Unable to export track");
|
Log.e(TAG, "Unable to export track");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFile.getName(), e);
|
Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFileUri, e);
|
||||||
return false;
|
return false;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Unable to close exportDocumentFile output stream", 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) {
|
public static boolean isExportFileExists(Track.Id trackId, String trackFileFormatExtension, List<String> filesName) {
|
||||||
String fileName = getExportFileNameByTrackId(trackId, trackFileFormatExtension);
|
return filesName.contains(getExportFileNameByTrackId(trackId, trackFileFormatExtension));
|
||||||
DocumentFile file = directory.findFile(fileName);
|
}
|
||||||
if (file == null) {
|
|
||||||
file = directory.createFile(mimeType, fileName);
|
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) {
|
private static String getExportFileNameByTrackId(Track.Id trackId, String trackFileFormatExtension) {
|
||||||
return trackId.getId() + "." + 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include layout="@layout/toolbar" />
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/export_progress_done"
|
||||||
|
style="@style/TextMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/export_progress_slash"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/export_progress_slash"
|
||||||
|
style="@style/TextMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/generic_separator_done_total"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/export_progress_done"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/export_progress_total"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/export_progress_done"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/export_progress_done" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/export_progress_total"
|
||||||
|
style="@style/TextMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/export_progress_slash"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/export_progress_slash"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/export_progress_slash"
|
||||||
|
tools:text="10" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/export_progress_bar"
|
||||||
|
style="@style/ProgressBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/export_progress_done"
|
||||||
|
tools:progress="25" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/export_progress_summary"
|
||||||
|
style="@style/TextMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:text="@string/export_progress_review"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/export_progress_bar" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/export_progress_alert_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/export_progress_alert_msg"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/export_progress_alert_msg"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/export_progress_alert_msg"
|
||||||
|
app:srcCompat="@drawable/ic_report_problem_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/export_progress_alert_msg"
|
||||||
|
style="@style/TextMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/export_progress_toggle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/export_progress_alert_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/export_progress_summary"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="The track already exists" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/export_progress_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:checked="false"
|
||||||
|
android:text="@string/export_all_do_it_for_all"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/export_progress_alert_msg" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<View style="@style/BottomButtonDivider" />
|
||||||
|
|
||||||
|
<LinearLayout style="@style/BottomButtonBar">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/export_progress_left_button"
|
||||||
|
style="@style/BottomButton"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/generic_skip"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/export_progress_right_button"
|
||||||
|
style="@style/BottomButton"
|
||||||
|
android:layout_gravity="right"
|
||||||
|
android:text="@string/generic_overwrite"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -216,6 +216,23 @@ limitations under the License.
|
|||||||
<string name="export_progress_message">Exporting to %1$s…</string>
|
<string name="export_progress_message">Exporting to %1$s…</string>
|
||||||
<string name="export_success">Exported %1$s to %2$s</string>
|
<string name="export_success">Exported %1$s to %2$s</string>
|
||||||
<string name="export_title">Export to…</string>
|
<string name="export_title">Export to…</string>
|
||||||
|
<string name="export_track_already_exists_msg">The track %1$s already exists at the destination directory.</string>
|
||||||
|
<plurals name="export_all_end_with_errors">
|
||||||
|
<item quantity="one">Completed with one error</item>
|
||||||
|
<item quantity="other">Completed with %1$d errors</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="export_all_end">Completed</string>
|
||||||
|
<string name="export_all_do_it_for_all">do it for all conflicts</string>
|
||||||
|
<string name="export_click_twice_cancel">Click back again to cancel</string>
|
||||||
|
<string name="export_show_errors">Show Errors</string>
|
||||||
|
<string name="export_track_errors">Tracks not exported</string>
|
||||||
|
<string name="export_progress_review">
|
||||||
|
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
|
||||||
|
</string>
|
||||||
<!-- External Storage -->
|
<!-- External Storage -->
|
||||||
<!-- Files -->
|
<!-- Files -->
|
||||||
<string name="files_few">%1$d files</string>
|
<string name="files_few">%1$d files</string>
|
||||||
@@ -243,6 +260,9 @@ limitations under the License.
|
|||||||
<string name="generic_success_title">Success</string>
|
<string name="generic_success_title">Success</string>
|
||||||
<string name="generic_tracks">Tracks</string>
|
<string name="generic_tracks">Tracks</string>
|
||||||
<string name="generic_yes">Yes</string>
|
<string name="generic_yes">Yes</string>
|
||||||
|
<string name="generic_overwrite">Overwrite</string>
|
||||||
|
<string name="generic_skip">Skip</string>
|
||||||
|
<string name="generic_separator_done_total">/</string>
|
||||||
<!-- Gps -->
|
<!-- Gps -->
|
||||||
<string name="gps_location_access">‘Location access’</string>
|
<string name="gps_location_access">‘Location access’</string>
|
||||||
<string name="gps_disabled">GPS not enabled. Tap here to go to %1$s. Press the back button to return.</string>
|
<string name="gps_disabled">GPS not enabled. Tap here to go to %1$s. Press the back button to return.</string>
|
||||||
|
|||||||
@@ -224,4 +224,6 @@ limitations under the License.
|
|||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="android:layout_width">wrap_content</item>
|
<item name="android:layout_width">wrap_content</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="ProgressBar" parent="@style/Widget.AppCompat.ProgressBar.Horizontal" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user