diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 10b7451b4..6c8d740ff 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -395,11 +395,6 @@ limitations under the License. android:icon="@drawable/ic_logo_color_24dp" android:label="@string/recording_service" /> - - documentUris = new ArrayList<>(); private boolean isDirectory; - private ImportViewModel viewModel; - private ImportViewModel.Summary summary; + private Importer importer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewBinding = ImportActivityBinding.inflate(getLayoutInflater()); - setContentView(viewBinding.getRoot()); final Intent intent = getIntent(); final ClipData intentClipData = intent.getClipData(); @@ -99,16 +95,36 @@ public class ImportActivity extends FragmentActivity { documentFiles = documentUris.stream().map(it -> DocumentFile.fromSingleUri(this, it)).collect(Collectors.toList()); } - viewModel = new ViewModelProvider(this).get(ImportViewModel.class); - viewModel.getImportData(documentFiles).observe(this, data -> { - summary = data; - setProgress(); - }); + importer = new Importer(this, documentFiles); + importer.getLiveData() + .observe(this, summary -> { + setProgress(summary); + }); + importer.startImport(); //Works for a directory, but we might have received multiple files via SEND_MULTIPLE. viewBinding.importActivityToolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName())); } + @NonNull + @Override + protected View createRootView() { + viewBinding = ImportActivityBinding.inflate(getLayoutInflater()); + return viewBinding.getRoot(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + viewBinding = null; + } + + @Override + public boolean onSupportNavigateUp() { + importer.cancel(); + return super.onSupportNavigateUp(); + } + @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); @@ -116,7 +132,7 @@ public class ImportActivity extends FragmentActivity { outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory); } - private void setProgress() { + private void setProgress(Importer.Summary summary) { int done = summary.getCount(); viewBinding.importProgressDone.setText("" + done); viewBinding.importProgressTotal.setText("" + summary.getTotalCount()); @@ -130,11 +146,11 @@ public class ImportActivity extends FragmentActivity { viewBinding.importProgressSummaryErrorsGroup.setVisibility(summary.getErrorCount() > 0 ? View.VISIBLE : View.GONE); if (summary.isDone()) { - onImportEnded(); + onImportEnded(summary); } } - private void onImportEnded() { + private void onImportEnded(Importer.Summary summary) { viewBinding.importProgressAlertMsg.setVisibility(View.VISIBLE); viewBinding.importProgressAlertIcon.setVisibility(View.VISIBLE); @@ -146,13 +162,13 @@ public class ImportActivity extends FragmentActivity { }); if (summary.getErrorCount() > 0) { - toggleUIEndWithErrors(); + toggleUIEndWithErrors(summary); } else { - toggleUIEndOk(); + toggleUIEndOk(summary); } } - private void toggleUIEndWithErrors() { + private void toggleUIEndWithErrors(Importer.Summary summary) { viewBinding.importProgressLeftButton.setVisibility(View.VISIBLE); viewBinding.importProgressLeftButton.setText(getString(R.string.generic_show_errors)); viewBinding.importProgressLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.import_error_list_dialog_title), summary.getFileErrors())); @@ -160,7 +176,7 @@ public class ImportActivity extends FragmentActivity { viewBinding.importProgressAlertMsg.setText(getResources().getQuantityString(R.plurals.generic_completed_with_errors, summary.getErrorCount(), summary.getErrorCount())); } - private void toggleUIEndOk() { + private void toggleUIEndOk(Importer.Summary summary) { viewBinding.importProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_dialog_success_24dp)); viewBinding.importProgressAlertMsg.setText(getString(R.string.generic_completed)); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportService.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportService.java deleted file mode 100644 index 5c9dab50c..000000000 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportService.java +++ /dev/null @@ -1,97 +0,0 @@ -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.util.ArrayList; - -import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.data.ContentProviderUtils; -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.io.file.TrackFileFormat; -import de.dennisguse.opentracks.settings.PreferencesUtils; -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) { - ArrayList trackIds = new ArrayList<>(); - - String fileExtension = FileUtils.getExtension(file); - try { - Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(); - boolean preventReimport = PreferencesUtils.getPreventReimportTracks(); - - TrackImporter trackImporter = new TrackImporter(this, new ContentProviderUtils(this), maxRecordingDistance, preventReimport); - - if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) { - trackIds.addAll(new XMLImporter(new GPXTrackImporter(this, trackImporter)).importFile(this, file.getUri())); - } else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) { - trackIds.addAll(new XMLImporter(new KMLTrackImporter(this, trackImporter)).importFile(this, file.getUri())); - } else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) { - trackIds.addAll(new KMZTrackImporter(this, trackImporter).importFile(file.getUri())); - } else { - Log.d(TAG, "Unsupported file format."); - sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file, getString(R.string.import_unsupported_format)); - return; - } - - if (!trackIds.isEmpty()) { - sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackIds, file, getString(R.string.import_file_imported, file.getName())); - } else { - sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackIds, file, 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, 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, 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, e.getMessage()); - } - } - - private void sendResult(int resultCode, ArrayList trackId, DocumentFile file, String message) { - Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID, trackId); - bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, file.getName()); - bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message); - resultReceiver.send(resultCode, bundle); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportServiceResultReceiver.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportServiceResultReceiver.java deleted file mode 100644 index e31631417..000000000 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportServiceResultReceiver.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.dennisguse.opentracks.io.file.importer; - -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; - -import androidx.annotation.NonNull; - -/** - * Create a new ResultReceive to receive results. - * Your {@link #onReceiveResult} method will be called from the thread running handler if given, or from an arbitrary thread if null. - */ -public class 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_LIST_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 final Receiver receiver; - - public ImportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) { - super(handler); - this.receiver = receiver; - } - - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - receiver.onReceiveResult(resultCode, resultData); - } - - public interface Receiver { - void onReceiveResult(int resultCode, Bundle resultData); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportViewModel.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportViewModel.java deleted file mode 100644 index 39b0fa5d3..000000000 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportViewModel.java +++ /dev/null @@ -1,138 +0,0 @@ -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 java.util.stream.Collectors; - -import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.data.models.Track; -import de.dennisguse.opentracks.util.FileUtils; - -public class ImportViewModel extends AndroidViewModel implements ImportServiceResultReceiver.Receiver { - - private static final String TAG = ImportViewModel.class.getSimpleName(); - - private MutableLiveData importData; - private final ImportServiceResultReceiver resultReceiver; - private final Summary summary; - private boolean cancel = false; - private final List filesToImport = new ArrayList<>(); - - public ImportViewModel(@NonNull Application application) { - super(application); - resultReceiver = new ImportServiceResultReceiver(new Handler(), this); - summary = new Summary(); - } - - LiveData getImportData(List documentFiles) { - if (importData == null) { - importData = new MutableLiveData<>(); - loadData(documentFiles); - } - return importData; - } - - void cancel() { - cancel = true; - } - - private void loadData(List documentFiles) { - List> nestedFileList = documentFiles.stream() - .map(FileUtils::getFiles) - .collect(Collectors.toList()); - - List fileList = new ArrayList<>(); - nestedFileList.forEach(fileList::addAll); - - summary.totalCount = fileList.size(); - filesToImport.addAll(fileList); - 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"); - } - - ArrayList trackIds = resultData.getParcelableArrayList(ImportServiceResultReceiver.RESULT_EXTRA_LIST_TRACK_ID); - 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)); - } - case ImportServiceResultReceiver.RESULT_CODE_IMPORTED -> { - summary.importedTrackIds.addAll(trackIds); - summary.successCount++; - } - case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS -> summary.existsCount++; - default -> - throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode); - } - - importData.postValue(summary); - importNextFile(); - } - - static class Summary { - private int totalCount; - private int successCount; - private int existsCount; - private int errorCount; - private final ArrayList importedTrackIds = new ArrayList<>(); - private final ArrayList 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 getImportedTrackIds() { - return importedTrackIds; - } - - public ArrayList getFileErrors() { - return fileErrors; - } - - public int getCount() { - return getSuccessCount() + getExistsCount() + getErrorCount(); - } - - public boolean isDone() { - return getTotalCount() == getCount(); - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportWorker.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportWorker.java new file mode 100644 index 000000000..9ee0fcfbc --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportWorker.java @@ -0,0 +1,100 @@ +package de.dennisguse.opentracks.io.file.importer; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.io.IOException; +import java.util.ArrayList; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.io.file.TrackFileFormat; +import de.dennisguse.opentracks.settings.PreferencesUtils; +import de.dennisguse.opentracks.util.FileUtils; + +public class ImportWorker extends Worker { + + private static final String TAG = ImportWorker.class.getSimpleName(); + + static final String URI_KEY = "DIRECTORY_URI_KEY"; + + static final String RESULT_SUCCESS_LIST_TRACKIDS_KEY = "RESULT_TRACK_IDS"; + static final String RESULT_URI_KEY = "RESULT_URI"; + static final String RESULT_MESSAGE_KEY = "RESULT_MESSAGE"; + static final String RESULT_FAILURE_IS_DUPLICATE = "RESULT_FAILURE_IS_DUPLICATE"; + + private final Uri uri; + + public ImportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + + uri = Uri.parse(getInputData().getString(URI_KEY)); + } + + @NonNull + @Override + public Result doWork() { + ArrayList trackIds = new ArrayList<>(); + Context context = getApplicationContext(); + + Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(); + boolean preventReimport = PreferencesUtils.getPreventReimportTracks(); + TrackImporter trackImporter = new TrackImporter(context, new ContentProviderUtils(context), maxRecordingDistance, preventReimport); + + Data.Builder data = new Data.Builder() + .putString(RESULT_URI_KEY, uri.toString()); + + + String fileExtension = FileUtils.getExtension(uri); + try { + if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) { + trackIds.addAll(new XMLImporter(new GPXTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri)); + } else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) { + trackIds.addAll(new XMLImporter(new KMLTrackImporter(getApplicationContext(), trackImporter)).importFile(context, uri)); + } else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) { + trackIds.addAll(new KMZTrackImporter(context, trackImporter).importFile(uri)); + } else { + Log.d(TAG, "Unsupported file format."); + return Result.failure(data + .putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unsupported_format)) + .build()); + } + + if (!trackIds.isEmpty()) { + return Result.success(data + .putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_file_imported, uri.toString())) //TODO unused? + .putLongArray(RESULT_SUCCESS_LIST_TRACKIDS_KEY, trackIds.stream().mapToLong(Track.Id::id).toArray()) + .build()); + } + + return Result.failure(data + .putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, uri.toString())) + .build()); + + } catch (IOException e) { + Log.d(TAG, "Unable to import file", e); + return Result.failure(data + .putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_unable_to_import_file, e.getMessage())) + .build()); + } catch (ImportParserException e) { + Log.d(TAG, "Parser error: " + e.getMessage(), e); + return Result.failure(data + .putString(RESULT_MESSAGE_KEY, context.getString(R.string.import_parser_error, e.getMessage())) + .build()); + } catch (ImportAlreadyExistsException e) { + Log.d(TAG, "Track already exists: " + e.getMessage(), e); + return Result.failure(data + .putString(RESULT_MESSAGE_KEY, e.getMessage()) //TODO This is not used + .putBoolean(RESULT_FAILURE_IS_DUPLICATE, true) + .build()); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/Importer.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/Importer.java new file mode 100644 index 000000000..d91547c00 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/Importer.java @@ -0,0 +1,151 @@ +package de.dennisguse.opentracks.io.file.importer; + +import androidx.documentfile.provider.DocumentFile; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.util.FileUtils; + +//TODO Cancel does not abort current import +//TODO Provide some intermediate progress (not only when a file got imported) +public class Importer { + + private final MutableLiveData liveData = new MutableLiveData<>(); + private final Summary summary = new Summary(); + private final List filesToImport = new ArrayList<>(); + + private boolean cancel = false; + + private final ImportActivity context; + private final WorkManager workManager; + + //TODO Add summary callback + Importer(ImportActivity context, List documentFiles) { + this.context = context; + this.workManager = WorkManager.getInstance(context); + + List> nestedFileList = documentFiles.stream() + .map(FileUtils::getFiles) + .collect(Collectors.toList()); + + List fileList = new ArrayList<>(); + nestedFileList.forEach(fileList::addAll); + + summary.totalCount = fileList.size(); + filesToImport.addAll(fileList); + } + + LiveData getLiveData() { + return liveData; + } + + void startImport() { + liveData.postValue(summary); + importNextFile(); + } + + void cancel() { + cancel = true; + } + + private void importNextFile() { + if (cancel || filesToImport.isEmpty()) { + return; + } + + final DocumentFile documentFile = filesToImport.get(0); + + WorkRequest importRequest = new OneTimeWorkRequest.Builder(ImportWorker.class) + .setInputData(new Data.Builder() + .putString(ImportWorker.URI_KEY, documentFile.getUri().toString()) + .build()) + .build(); + + workManager + .getWorkInfoByIdLiveData(importRequest.getId()) + .observe(context, workInfo -> { + if (workInfo != null) { + WorkInfo.State state = workInfo.getState(); + if (state.isFinished()) { + switch (state) { + case SUCCEEDED -> { + summary.importedTrackIds.addAll( + Arrays.stream(workInfo.getOutputData().getLongArray(ImportWorker.RESULT_SUCCESS_LIST_TRACKIDS_KEY)) + .mapToObj(Track.Id::new) + .toList()); + + summary.successCount++; + } + case FAILED -> { + if (workInfo.getOutputData().getBoolean(ImportWorker.RESULT_FAILURE_IS_DUPLICATE, false)) { + summary.existsCount++; + } else { + // Some error happened + String errorMessage = workInfo.getOutputData().getString(ImportWorker.RESULT_MESSAGE_KEY); + summary.fileErrors.add(context.getString(R.string.import_error_info, documentFile.getName(), errorMessage)); + } + } + } + + liveData.postValue(summary); + importNextFile(); + } + } + }); + + workManager.enqueue(importRequest); + filesToImport.remove(0); + } + + static class Summary { + private int totalCount; + private int successCount; + private int existsCount; + private final ArrayList importedTrackIds = new ArrayList<>(); + private final ArrayList fileErrors = new ArrayList<>(); + + public int getTotalCount() { + return totalCount; + } + + public int getSuccessCount() { + return successCount; + } + + public int getExistsCount() { + return existsCount; + } + + public int getErrorCount() { + return fileErrors.size(); + } + + public ArrayList getImportedTrackIds() { + return importedTrackIds; + } + + public ArrayList getFileErrors() { + return fileErrors; + } + + public int getCount() { + return getSuccessCount() + getExistsCount() + getErrorCount(); + } + + public boolean isDone() { + return getTotalCount() == getCount(); + } + } +}