forked from upstream-mirrors/OpenTracks
Activity with progress and summary for import all option. Fixes #471.
ErrorListDialog shared by ExportActivity and ImportActivity.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
@@ -9,7 +10,9 @@ import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@@ -59,6 +62,9 @@ public class ExportImportTest {
|
||||
private Track.Id importTrackId;
|
||||
private final Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
|
||||
@Rule
|
||||
public ExpectedException exception = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
|
||||
@@ -181,6 +187,8 @@ public class ExportImportTest {
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context);
|
||||
|
||||
exception.expect(ImportAlreadyExistsException.class);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
@@ -270,6 +278,8 @@ public class ExportImportTest {
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context);
|
||||
|
||||
exception.expect(ImportAlreadyExistsException.class);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
@@ -68,9 +68,7 @@ limitations under the License.
|
||||
|
||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".io.file.importer.ImportActivity"
|
||||
android:theme="@style/ThemeCustomTransparentActivity">
|
||||
<activity android:name=".io.file.importer.ImportActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
@@ -333,5 +331,10 @@ limitations under the License.
|
||||
android:name=".io.file.exporter.ExportService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".io.file.importer.ImportService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,111 +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 ImportFinishedDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String TAG = ImportFinishedDialogFragment.class.getSimpleName();
|
||||
|
||||
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
|
||||
private static final String EXTRA_IMPORTED_TRACK_COUNT = "imported_track_count";
|
||||
private static final String EXTRA_FILE_COUNT = "file_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_IMPORTED_TRACK_COUNT, successCount);
|
||||
bundle.putInt(EXTRA_FILE_COUNT, fileCount);
|
||||
|
||||
ImportFinishedDialogFragment dialogFragment = new ImportFinishedDialogFragment();
|
||||
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 importedTrackCount = getArguments().getInt(EXTRA_IMPORTED_TRACK_COUNT);
|
||||
int totalTrackCount = getArguments().getInt(EXTRA_FILE_COUNT);
|
||||
|
||||
int iconId;
|
||||
int titleId;
|
||||
String message;
|
||||
String totalFiles = getResources().getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
|
||||
if (importedTrackCount == totalTrackCount) {
|
||||
if (totalTrackCount == 0) {
|
||||
iconId = R.drawable.ic_dialog_info_24dp;
|
||||
titleId = R.string.import_no_file_title;
|
||||
message = getString(R.string.import_no_file, directoryDisplayName);
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_success_24dp;
|
||||
titleId = R.string.generic_success_title;
|
||||
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
|
||||
}
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_error_24dp;
|
||||
titleId = R.string.generic_error_title;
|
||||
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
|
||||
}
|
||||
Dialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setCancelable(true).setIcon(iconId)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
|
||||
dialogInterface.dismiss();
|
||||
caller.onDismissed();
|
||||
})
|
||||
.setTitle(titleId)
|
||||
.create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onDismissed();
|
||||
}
|
||||
}
|
||||
@@ -1,230 +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.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.importer.GpxFileTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.KmlFileTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.KmzTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackImporter;
|
||||
import de.dennisguse.opentracks.util.DialogUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportProgressDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String EXTRA_IS_TREE_KEY = "tree_uri";
|
||||
|
||||
private static final String EXTRA_DATA_URI_KEY = "data_uri";
|
||||
|
||||
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
|
||||
|
||||
public static final String IMPORT_DIALOG_TAG = "import_dialog_tag";
|
||||
|
||||
private DismissCallback caller;
|
||||
|
||||
private ImportThread importThread;
|
||||
|
||||
private String directoryDisplayName;
|
||||
|
||||
private int trackImportSuccessCount;
|
||||
private int fileCount;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri, boolean isDirectory) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(EXTRA_DATA_URI_KEY, documentFileUri);
|
||||
bundle.putBoolean(EXTRA_IS_TREE_KEY, isDirectory);
|
||||
|
||||
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
|
||||
dialogFragment.setArguments(bundle);
|
||||
dialogFragment.setRetainInstance(true);
|
||||
dialogFragment.show(fragmentManager, IMPORT_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_DATA_URI_KEY);
|
||||
DocumentFile documentFile;
|
||||
if (getArguments().getBoolean(EXTRA_IS_TREE_KEY)) {
|
||||
documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
|
||||
} else {
|
||||
documentFile = DocumentFile.fromSingleUri(getContext(), directoryUri);
|
||||
}
|
||||
directoryDisplayName = FileUtils.getPath(documentFile);
|
||||
|
||||
importThread = new ImportThread(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.import_progress_message,
|
||||
dialogInterface -> {
|
||||
importThread.interrupt();
|
||||
dialogInterface.dismiss();
|
||||
caller.onImportCanceled(directoryDisplayName, trackImportSuccessCount, fileCount);
|
||||
}, directoryDisplayName);
|
||||
|
||||
progressDialog.setIndeterminate(false);
|
||||
progressDialog.setMax(fileCount);
|
||||
progressDialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
setProgress();
|
||||
if (!importThread.isAlive()) {
|
||||
importThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!importThread.isAlive()) {
|
||||
importThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
private void onProgress(int trackImportSuccessCount, int fileCount) {
|
||||
this.trackImportSuccessCount = trackImportSuccessCount;
|
||||
this.fileCount = fileCount;
|
||||
setProgress();
|
||||
}
|
||||
|
||||
private void setProgress() {
|
||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.setIndeterminate(false);
|
||||
dialog.setMax(fileCount);
|
||||
dialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportCompleted(int successCount, int fileCount) {
|
||||
if (caller != null && !isDetached()) {
|
||||
caller.onImportFinished(directoryDisplayName, successCount, fileCount);
|
||||
}
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
|
||||
private class ImportThread extends Thread {
|
||||
|
||||
private final DocumentFile file;
|
||||
|
||||
ImportThread(DocumentFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Context context = ImportProgressDialogFragment.this.getContext();
|
||||
|
||||
List<DocumentFile> files = FileUtils.getFiles(file);
|
||||
|
||||
int importedTrackCount = 0;
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
onProgress(i, files.size());
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
return;
|
||||
}
|
||||
if (importFile(context, files.get(i))) {
|
||||
importedTrackCount++;
|
||||
}
|
||||
}
|
||||
|
||||
onImportCompleted(importedTrackCount, files.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file.
|
||||
*
|
||||
* @param file the file
|
||||
*/
|
||||
private boolean importFile(final Context context, final DocumentFile file) {
|
||||
TrackImporter trackImporter;
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new GpxFileTrackImporter(context);
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext());
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmzTrackImporter(context, file.getUri());
|
||||
} else {
|
||||
Log.i(TAG, "Unsupported file format.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
|
||||
return trackImporter.importFile(inputStream) != null;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onImportCanceled(String directoryDisplayName, int successCount, int fileCount);
|
||||
|
||||
void onImportFinished(String directoryDisplayName, int successCount, int fileCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.dennisguse.opentracks.io.file;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class ErrorListDialog extends DialogFragment {
|
||||
|
||||
public static final String TAG = ErrorListDialog.class.getSimpleName();
|
||||
|
||||
private static final String EXTRA_TITLE = "extra_title";
|
||||
private static final String EXTRA_ERROR_LIST = "extra_error_list";
|
||||
|
||||
private ArrayList<String> errorList;
|
||||
private String title;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, String title, ArrayList<String> errorList) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_TITLE, title);
|
||||
bundle.putStringArrayList(EXTRA_ERROR_LIST, errorList);
|
||||
|
||||
ErrorListDialog errorListDialog = new ErrorListDialog();
|
||||
errorListDialog.setArguments(bundle);
|
||||
errorListDialog.setRetainInstance(true);
|
||||
errorListDialog.show(fragmentManager, TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
title = getArguments().getString(EXTRA_TITLE);
|
||||
errorList = getArguments().getStringArrayList(EXTRA_ERROR_LIST);
|
||||
} else {
|
||||
title = savedInstanceState.getString(EXTRA_TITLE);
|
||||
errorList = savedInstanceState.getStringArrayList(EXTRA_ERROR_LIST);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(EXTRA_TITLE, title);
|
||||
outState.putStringArrayList(EXTRA_ERROR_LIST, errorList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final String[] tracks = errorList.stream().toArray(String[]::new);
|
||||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(title)
|
||||
.setItems(tracks, null)
|
||||
.setPositiveButton(R.string.generic_ok, (dialog, which) -> dismiss());
|
||||
AlertDialog dialog = alertDialogBuilder.create();
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
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;
|
||||
@@ -34,7 +32,6 @@ 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 java.util.ArrayList;
|
||||
@@ -45,6 +42,7 @@ 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.ErrorListDialog;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.ExportUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
@@ -68,6 +66,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
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 String BUNDLE_TRACK_ERRORS = "track_errors";
|
||||
|
||||
private static final int CONFLICT_NONE = 0;
|
||||
private static final int CONFLICT_OVERWRITE = 1;
|
||||
@@ -99,6 +98,8 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
private Button viewLeftButton;
|
||||
private Button viewRightButton;
|
||||
|
||||
private ArrayList<String> trackErrors = new ArrayList<>();
|
||||
|
||||
private int autoConflict;
|
||||
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -159,8 +160,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
|
||||
|
||||
resultReceiver = new ExportServiceResultReceiver(new Handler());
|
||||
resultReceiver.setReceiver(this);
|
||||
resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
|
||||
|
||||
viewTotal = findViewById(R.id.export_progress_total);
|
||||
viewDone = findViewById(R.id.export_progress_done);
|
||||
@@ -187,6 +187,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
trackExportSkippedCount = savedInstanceState.getInt(BUNDLE_SKIPPED_COUNT);
|
||||
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
|
||||
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
|
||||
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
|
||||
|
||||
setProgress();
|
||||
initExport(getTotalDone());
|
||||
@@ -203,6 +204,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
outState.putInt(BUNDLE_SKIPPED_COUNT, trackExportSkippedCount);
|
||||
outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
|
||||
outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles);
|
||||
outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -220,7 +222,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
}
|
||||
|
||||
this.doubleBackToCancel = true;
|
||||
Toast.makeText(this, getString(R.string.export_click_twice_cancel), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show();
|
||||
|
||||
new Handler().postDelayed(() -> doubleBackToCancel=false, 2000);
|
||||
}
|
||||
@@ -311,15 +313,15 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
|
||||
if (trackExportErrorCount > 0) {
|
||||
viewLeftButton.setVisibility(View.VISIBLE);
|
||||
viewLeftButton.setText(getString(R.string.export_show_errors));
|
||||
viewLeftButton.setOnClickListener((view) -> new ErrorListDialog().show(getSupportFragmentManager(), ErrorListDialog.TAG));
|
||||
viewLeftButton.setText(getString(R.string.generic_show_errors));
|
||||
viewLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.export_track_errors), trackErrors));
|
||||
viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
|
||||
String msg = getResources().getQuantityString(R.plurals.export_all_end_with_errors, trackExportErrorCount, trackExportErrorCount);
|
||||
String msg = getResources().getQuantityString(R.plurals.generic_completed_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));
|
||||
viewAlertMsg.setText(getString(R.string.generic_completed));
|
||||
}
|
||||
|
||||
viewRightButton.setOnClickListener((view) -> finish());
|
||||
@@ -337,7 +339,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
case ExportServiceResultReceiver.RESULT_CODE_ERROR:
|
||||
trackExportErrorCount++;
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
ErrorListDialog.trackErrors.add(track.getName());
|
||||
trackErrors.add(track.getName());
|
||||
break;
|
||||
case ExportServiceResultReceiver.RESULT_CODE_SUCCESS:
|
||||
if (ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles)) {
|
||||
@@ -413,22 +415,4 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -17,8 +17,9 @@ public class ExportServiceResultReceiver extends ResultReceiver {
|
||||
|
||||
private Receiver receiver;
|
||||
|
||||
public ExportServiceResultReceiver(Handler handler) {
|
||||
public ExportServiceResultReceiver(Handler handler, Receiver receiver) {
|
||||
super(handler);
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,10 +29,6 @@ public class ExportServiceResultReceiver extends ResultReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
public void setReceiver(Receiver receiver) {
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
void onReceiveResult(int resultCode, Bundle resultData);
|
||||
}
|
||||
|
||||
+8
-8
@@ -134,13 +134,18 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
saxParser.parse(inputStream, this);
|
||||
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
||||
if (trackIds.size() != 1) {
|
||||
throw new MultiTracksImportException();
|
||||
// TODO Multi track is not supported yet.
|
||||
throw new ImportParserException("Multi track not supported");
|
||||
}
|
||||
return trackIds.get(0);
|
||||
} catch (MultiTracksImportException | IOException | SAXException | SQLiteConstraintException | ParserConfigurationException e) {
|
||||
} catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
return null;
|
||||
throw new ImportParserException(e);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
throw new ImportAlreadyExistsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,9 +555,4 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
// The number of buffered locations
|
||||
int numBufferedTrackPoints = 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//TODO According to the reader AND writer, we support multiple tracks per file; we should implement it here also.
|
||||
private static class MultiTracksImportException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,62 +16,178 @@
|
||||
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
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.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import de.dennisguse.opentracks.fragments.ImportFinishedDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ImportProgressDialogFragment;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||
|
||||
/**
|
||||
* An activity to import files from the external storage.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ImportActivity extends FragmentActivity implements ImportProgressDialogFragment.DismissCallback, ImportFinishedDialogFragment.DismissCallback {
|
||||
public class ImportActivity extends FragmentActivity {
|
||||
|
||||
private static final String TAG = ImportActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
||||
|
||||
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
|
||||
private static final String BUNDLE_TOOLBAR_TITLE = "toolbar_title";
|
||||
private static final String BUNDLE_DOCUMENT_URI = "document_uri";
|
||||
private static final String BUNDLE_IS_DIRECTORY = "is_directory";
|
||||
|
||||
private TextView viewTotal;
|
||||
private TextView viewDone;
|
||||
private TextView viewSummary;
|
||||
private ProgressBar viewProgressBar;
|
||||
private ImageView viewAlertIcon;
|
||||
private TextView viewAlertMsg;
|
||||
private Button viewLeftButton;
|
||||
private Button viewRightButton;
|
||||
|
||||
boolean doubleBackToCancel = false;
|
||||
|
||||
private Uri documentUri;
|
||||
private boolean isDirectory;
|
||||
private String toolbarTitle;
|
||||
|
||||
private ImportViewModel viewModel;
|
||||
private ImportViewModel.Summary summary;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.import_progress_activity);
|
||||
|
||||
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
viewTotal = findViewById(R.id.import_progress_total);
|
||||
viewDone = findViewById(R.id.import_progress_done);
|
||||
viewSummary = findViewById(R.id.import_progress_summary);
|
||||
viewProgressBar = findViewById(R.id.import_progress_bar);
|
||||
viewAlertIcon = findViewById(R.id.import_progress_alert_icon);
|
||||
viewAlertMsg = findViewById(R.id.import_progress_alert_msg);
|
||||
viewLeftButton = findViewById(R.id.import_progress_left_button);
|
||||
viewRightButton = findViewById(R.id.import_progress_right_button);
|
||||
|
||||
final DocumentFile documentFile;
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
if (getIntent().getData() != null) {
|
||||
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getData(), false);
|
||||
documentUri = getIntent().getData();
|
||||
isDirectory = false;
|
||||
} else if (getIntent().getClipData() != null && getIntent().getClipData().getItemCount() > 0) {
|
||||
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getClipData().getItemAt(0).getUri(), false);
|
||||
documentUri = getIntent().getClipData().getItemAt(0).getUri();
|
||||
isDirectory = false;
|
||||
} else {
|
||||
// Started from DirectoryChooserActivity
|
||||
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY), true);
|
||||
documentUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
||||
isDirectory = true;
|
||||
}
|
||||
|
||||
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
|
||||
toolbarTitle = getString(R.string.import_progress_message, documentFile.getName());
|
||||
} else {
|
||||
documentUri = savedInstanceState.getParcelable(BUNDLE_DOCUMENT_URI);
|
||||
toolbarTitle = savedInstanceState.getString(BUNDLE_TOOLBAR_TITLE);
|
||||
isDirectory = savedInstanceState.getBoolean(BUNDLE_IS_DIRECTORY);
|
||||
|
||||
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
|
||||
}
|
||||
|
||||
toolbar.setTitle(toolbarTitle);
|
||||
initViews();
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
|
||||
viewModel.getImportData(documentFile).observe(this, data -> {
|
||||
summary = data;
|
||||
setProgress();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
|
||||
outState.putParcelable(BUNDLE_DOCUMENT_URI, documentUri);
|
||||
outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory);
|
||||
outState.putString(BUNDLE_TOOLBAR_TITLE, toolbarTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportCanceled(String directoryDisplayName, int successCount, int fileCount) {
|
||||
onImportFinished(directoryDisplayName, successCount, fileCount);
|
||||
public void onBackPressed() {
|
||||
if (doubleBackToCancel || getTotalDone() == summary.getTotalCount()) {
|
||||
super.onBackPressed();
|
||||
viewModel.cancel();
|
||||
getViewModelStore().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.doubleBackToCancel = true;
|
||||
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show();
|
||||
|
||||
new Handler().postDelayed(() -> doubleBackToCancel=false, 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
|
||||
runOnUiThread(() -> ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
|
||||
private void initViews() {
|
||||
viewDone.setText("0");
|
||||
viewTotal.setText("0");
|
||||
viewSummary.setText(getString(R.string.import_progress_review, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed() {
|
||||
finish();
|
||||
private int getTotalDone() {
|
||||
return summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount();
|
||||
}
|
||||
|
||||
private void setProgress() {
|
||||
int done = getTotalDone();
|
||||
|
||||
viewDone.setText("" + done);
|
||||
viewTotal.setText("" + summary.getTotalCount());
|
||||
|
||||
viewProgressBar.setProgress((int) ((float) done / (float) summary.getTotalCount() * 100f));
|
||||
viewSummary.setText(getString(R.string.import_progress_review, getTotalDone(), summary.getSuccessCount(), summary.getExistsCount(), summary.getErrorCount()));
|
||||
|
||||
if (done == summary.getTotalCount()) {
|
||||
onImportEnded();
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportEnded() {
|
||||
viewAlertIcon.setVisibility(View.VISIBLE);
|
||||
viewAlertMsg.setVisibility(View.VISIBLE);
|
||||
viewRightButton.setVisibility(View.VISIBLE);
|
||||
viewRightButton.setText(getString(R.string.generic_ok));
|
||||
|
||||
if (summary.getErrorCount() > 0) {
|
||||
viewLeftButton.setVisibility(View.VISIBLE);
|
||||
viewLeftButton.setText(getString(R.string.generic_show_errors));
|
||||
viewLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.import_error_list_dialog_title), summary.getFileErrors()));
|
||||
viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
|
||||
String msg = getResources().getQuantityString(R.plurals.generic_completed_with_errors, summary.getErrorCount(), summary.getErrorCount());
|
||||
viewAlertMsg.setText(msg);
|
||||
} else {
|
||||
viewLeftButton.setVisibility(View.GONE);
|
||||
viewAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_dialog_success_24dp));
|
||||
viewAlertMsg.setText(getString(R.string.generic_completed));
|
||||
}
|
||||
|
||||
viewRightButton.setOnClickListener((view) -> {
|
||||
getViewModelStore().clear();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
class ImportAlreadyExistsException extends RuntimeException {
|
||||
public ImportAlreadyExistsException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
class ImportParserException extends RuntimeException {
|
||||
public ImportParserException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ImportParserException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
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 android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.JobIntentService;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportService extends JobIntentService {
|
||||
|
||||
private static final String TAG = ImportService.class.getSimpleName();
|
||||
|
||||
private static final int JOB_ID = 2;
|
||||
|
||||
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||
private static final String EXTRA_URI = "extra_uri";
|
||||
|
||||
private ResultReceiver resultReceiver;
|
||||
|
||||
public static void enqueue(Context context, ImportServiceResultReceiver receiver, Uri uri) {
|
||||
Intent intent = new Intent(context, JobService.class);
|
||||
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||
intent.putExtra(EXTRA_URI, uri);
|
||||
enqueueWork(context, ImportService.class, JOB_ID, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleWork(@NonNull Intent intent) {
|
||||
resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||
Uri uri = intent.getParcelableExtra(EXTRA_URI);
|
||||
importFile(DocumentFile.fromSingleUri(this, uri));
|
||||
}
|
||||
|
||||
private void importFile(DocumentFile file) {
|
||||
TrackImporter trackImporter;
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new GpxFileTrackImporter(this);
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmlFileTrackImporter(this);
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmzTrackImporter(this, file.getUri());
|
||||
} else {
|
||||
Log.d(TAG, "Unsupported file format.");
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unsupported_format));
|
||||
return;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
|
||||
Track.Id trackId = trackImporter.importFile(inputStream);
|
||||
if (trackId != null) {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file.getName(), getString(R.string.import_file_imported, file.getName()));
|
||||
} else {
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file.getName(), getString(R.string.import_unable_to_import_file, file.getName()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to import file", e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unable_to_import_file, e.getMessage()));
|
||||
} catch (ImportParserException e) {
|
||||
Log.d(TAG, "Parser error: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_parser_error, e.getMessage()));
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
|
||||
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendResult(int resultCode, Track.Id trackId, String fileName, String message) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, fileName);
|
||||
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
|
||||
resultReceiver.send(resultCode, bundle);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
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 ImportServiceResultReceiver extends ResultReceiver {
|
||||
|
||||
public static final int RESULT_CODE_ERROR = 0;
|
||||
public static final int RESULT_CODE_IMPORTED = 1;
|
||||
public static final int RESULT_CODE_ALREADY_EXISTS = 2;
|
||||
|
||||
public static final String RESULT_EXTRA_TRACK_ID = "result_track_id";
|
||||
public static final String RESULT_EXTRA_FILENAME = "result_extra_filename";
|
||||
public static final String RESULT_EXTRA_MESSAGE = "result_extra_message";
|
||||
|
||||
private Receiver receiver;
|
||||
|
||||
public ImportServiceResultReceiver(Handler handler, Receiver receiver) {
|
||||
super(handler);
|
||||
this.receiver = receiver;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (receiver != null) {
|
||||
receiver.onReceiveResult(resultCode, resultData);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Receiver {
|
||||
void onReceiveResult(int resultCode, Bundle resultData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportViewModel extends AndroidViewModel implements ImportServiceResultReceiver.Receiver {
|
||||
|
||||
private static final String TAG = ImportViewModel.class.getSimpleName();
|
||||
|
||||
private MutableLiveData<Summary> importData;
|
||||
private ImportServiceResultReceiver resultReceiver;
|
||||
private Summary summary;
|
||||
private boolean cancel = false;
|
||||
private List<DocumentFile> filesToImport = new ArrayList<>();
|
||||
|
||||
public ImportViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
resultReceiver = new ImportServiceResultReceiver(new Handler(), this);
|
||||
summary = new Summary();
|
||||
}
|
||||
|
||||
public LiveData<Summary> getImportData(DocumentFile documentFile) {
|
||||
if (importData == null) {
|
||||
importData = new MutableLiveData<>();
|
||||
loadData(documentFile);
|
||||
}
|
||||
return importData;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
private void loadData(DocumentFile documentFile) {
|
||||
List<DocumentFile> fileList = FileUtils.getFiles(documentFile);
|
||||
summary.totalCount = fileList.size();
|
||||
for (DocumentFile df : fileList) {
|
||||
filesToImport.add(df);
|
||||
}
|
||||
importNextFile();
|
||||
}
|
||||
|
||||
private void importNextFile() {
|
||||
if (cancel || filesToImport.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ImportService.enqueue(getApplication(), resultReceiver, filesToImport.get(0).getUri());
|
||||
filesToImport.remove(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (resultData == null) {
|
||||
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
|
||||
}
|
||||
|
||||
String fileName = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME);
|
||||
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
|
||||
|
||||
switch (resultCode) {
|
||||
case ImportServiceResultReceiver.RESULT_CODE_ERROR:
|
||||
summary.errorCount++;
|
||||
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
|
||||
break;
|
||||
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED:
|
||||
summary.successCount++;
|
||||
break;
|
||||
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS:
|
||||
summary.existsCount++;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
|
||||
}
|
||||
|
||||
importData.postValue(summary);
|
||||
importNextFile();
|
||||
}
|
||||
|
||||
public class Summary {
|
||||
private int totalCount;
|
||||
private int successCount;
|
||||
private int existsCount;
|
||||
private int errorCount;
|
||||
private ArrayList<String> fileErrors = new ArrayList<>();
|
||||
|
||||
public int getTotalCount() {
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public int getExistsCount() {
|
||||
return existsCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public ArrayList<String> getFileErrors() {
|
||||
return fileErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,13 @@ public class KmzTrackImporter implements TrackImporter {
|
||||
return null;
|
||||
}
|
||||
|
||||
trackId = findAndParseKmlFile(inputStream);
|
||||
try {
|
||||
trackId = findAndParseKmlFile(inputStream);
|
||||
} catch (Exception e) {
|
||||
cleanImport(context, importTrackId);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (trackId == null) {
|
||||
cleanImport(context, importTrackId);
|
||||
return null;
|
||||
@@ -184,9 +190,15 @@ public class KmzTrackImporter implements TrackImporter {
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
return trackId;
|
||||
} catch (ImportParserException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
throw e;
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
return null;
|
||||
throw new ImportParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,11 +254,17 @@ public class KmzTrackImporter implements TrackImporter {
|
||||
* @param zipInputStream the zip input stream
|
||||
* @return the imported track id or -1L
|
||||
*/
|
||||
private Track.Id parseKml(ZipInputStream zipInputStream) throws IOException {
|
||||
private Track.Id parseKml(ZipInputStream zipInputStream) {
|
||||
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
|
||||
|
||||
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
|
||||
return kmlFileTrackImporter.importFile(byteArrayInputStream);
|
||||
} catch (ImportParserException e) {
|
||||
throw e;
|
||||
} catch (ImportAlreadyExistsException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new ImportParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,27 +32,6 @@ 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) {
|
||||
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
|
||||
|
||||
@@ -81,6 +60,27 @@ public class ExportUtils {
|
||||
return filesName.contains(getExportFileNameByTrackId(trackId, trackFileFormatExtension));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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/import_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/import_progress_slash"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_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/import_progress_done"
|
||||
app:layout_constraintEnd_toStartOf="@id/import_progress_total"
|
||||
app:layout_constraintStart_toEndOf="@id/import_progress_done"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_done" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_total"
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/import_progress_slash"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/import_progress_slash"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_slash"
|
||||
tools:text="10" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/import_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/import_progress_done"
|
||||
tools:progress="25" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_progress_summary"
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:text="@string/import_progress_review"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/import_progress_bar" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/import_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/import_progress_alert_msg"
|
||||
app:layout_constraintEnd_toStartOf="@id/import_progress_alert_msg"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/import_progress_alert_msg"
|
||||
app:srcCompat="@drawable/ic_report_problem_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/import_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_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/import_progress_alert_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/import_progress_summary"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="The file already exists" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<View style="@style/BottomButtonDivider" />
|
||||
|
||||
<LinearLayout style="@style/BottomButtonBar">
|
||||
|
||||
<Button
|
||||
android:id="@+id/import_progress_left_button"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/generic_show_errors"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/import_progress_right_button"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="right"
|
||||
android:text="@string/generic_ok"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -217,14 +217,7 @@ limitations under the License.
|
||||
<string name="export_success">Exported %1$s to %2$s</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
|
||||
@@ -263,6 +256,13 @@ limitations under the License.
|
||||
<string name="generic_overwrite">Overwrite</string>
|
||||
<string name="generic_skip">Skip</string>
|
||||
<string name="generic_separator_done_total">/</string>
|
||||
<string name="generic_show_errors">Show Errors</string>
|
||||
<plurals name="generic_completed_with_errors">
|
||||
<item quantity="one">Completed with one error</item>
|
||||
<item quantity="other">Completed with %1$d errors</item>
|
||||
</plurals>
|
||||
<string name="generic_completed">Completed</string>
|
||||
<string name="generic_click_twice_cancel">Click back again to cancel</string>
|
||||
<!-- Gps -->
|
||||
<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>
|
||||
@@ -285,6 +285,12 @@ limitations under the License.
|
||||
<string name="image_stop">Stop</string>
|
||||
<string name="image_track">Track</string>
|
||||
<!-- Import -->
|
||||
<string name="import_unsupported_format">Unsupported file format</string>
|
||||
<string name="import_parser_error">Parser error: %1$s</string>
|
||||
<string name="import_unable_to_import_file">Unable to import file: %1$s</string>
|
||||
<string name="import_error_info">%1$s: %2$s</string>
|
||||
<string name="import_error_list_dialog_title">Files not imported</string>
|
||||
<string name="import_file_imported">File %1$s imported</string>
|
||||
<string name="import_error">Imported %1$d of %2$s from %3$s</string>
|
||||
<string name="import_no_directory">%1$s does not exist</string>
|
||||
<string name="import_no_file">There were no files to import. Connect your device to your computer and place the files to import in %1$s.</string>
|
||||
@@ -293,6 +299,12 @@ limitations under the License.
|
||||
<string name="import_selection_option">All %1$s</string>
|
||||
<string name="import_selection_title">Import from external storage</string>
|
||||
<string name="import_success">Imported %1$s from %2$s</string>
|
||||
<string name="import_progress_review">
|
||||
Total imported files: %1$d\n
|
||||
- Files imported: %2$d\n
|
||||
- Already exists: %3$d\n
|
||||
- Errors: %4$d
|
||||
</string>
|
||||
<!-- Marker -->
|
||||
<string name="marker_add_error">Unable to insert a marker. No GPS signal. Try again.</string>
|
||||
<string name="marker_add_success">A marker was inserted</string>
|
||||
|
||||
@@ -33,16 +33,6 @@ limitations under the License.
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeCustomTransparentActivity" parent="ThemeCustom">
|
||||
<!-- TODO Remove when moving to settings to fragments. -->
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
<style name="TrackDeleteActivity" parent="ThemeCustom">
|
||||
<!-- TODO Remove when moving to TrackDeleteActivity to fragments. -->
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
|
||||
Reference in New Issue
Block a user