From 02a77e5ce7613f1c7a89f688d68df2bb4978d494 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sat, 28 Jun 2025 17:11:27 +0200 Subject: [PATCH] Migrate ExportService to WorkManager. Part of #1315. --- src/main/AndroidManifest.xml | 5 - .../opentracks/TrackStoppedActivity.java | 2 +- .../io/file/exporter/ExportActivity.java | 49 +++++++-- .../io/file/exporter/ExportService.java | 103 ------------------ .../file/exporter}/ExportUtils.java | 85 ++++++++++++--- .../io/file/exporter/ExportWorker.java | 70 ++++++++++++ .../opentracks/publicapi/StopRecording.java | 2 +- 7 files changed, 177 insertions(+), 139 deletions(-) delete mode 100644 src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java rename src/main/java/de/dennisguse/opentracks/{util => io/file/exporter}/ExportUtils.java (60%) create mode 100644 src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportWorker.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 2b3327011..10b7451b4 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" /> - - directoryFiles; private int trackExportSuccessCount; @@ -161,8 +164,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri); String directoryDisplayName = FileUtils.getPath(documentFile); - resultReceiver = new ExportService.ExportServiceResultReceiver(new Handler(), this); - if (savedInstanceState == null) { autoConflict = ConflictResolutionStrategy.CONFLICT_NONE; setProgress(); @@ -231,19 +232,45 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_NONE) { conflict(exportTask); - } else if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) { + return; + } + if (fileExists && conflictResolution == ConflictResolutionStrategy.CONFLICT_SKIP) { trackExportSkippedCount++; nextExport(exportTask); - } else { - ExportService.enqueue(this, resultReceiver, exportTask, directoryUri); + return; } + + WorkManager workManager = WorkManager.getInstance(this); + WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class) + .setInputData(new Data.Builder() + .putLongArray(ExportWorker.TRACKIDS_KEY, exportTask.getTrackIds().stream().mapToLong(Track.Id::id).toArray()) + .putString(ExportWorker.DIRECTORY_URI_KEY, directoryUri.toString()) + .putString(ExportWorker.TRACKFILEFORMAT_KEY, exportTask.getTrackFileFormat().toString()) + .putString(ExportWorker.FILENAME_KEY, exportTask.getFilename()) + .build()) + .build(); + + workManager + .getWorkInfoByIdLiveData(exportRequest.getId()) + .observe(this, workInfo -> { + if (workInfo != null) { + WorkInfo.State state = workInfo.getState(); + switch (state) { + case SUCCEEDED -> onExportSuccess(exportTask); + case FAILED -> + onExportError(exportTask, workInfo.getOutputData().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY)); + } + } + }); + + workManager.enqueue(exportRequest); } private void export(ExportTask exportTask) { export(exportTask, autoConflict); } - @Deprecated //TODO Check should be done in ExportService + @Deprecated //TODO Check should be done in ExportWorker private boolean exportFileExists(ExportTask exportTask) { String filename; if (exportTask.isMultiExport()) { @@ -314,7 +341,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E } } - @Override public void onExportSuccess(ExportTask exportTask) { if (exportFileExists(exportTask)) { trackExportOverwrittenCount++; @@ -325,7 +351,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E nextExport(exportTask); } - @Override public void onExportError(ExportTask exportTask, String errorMessage) { trackExportErrorCount++; String name; diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java deleted file mode 100644 index 871a3cb53..000000000 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java +++ /dev/null @@ -1,103 +0,0 @@ -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.Handler; -import android.os.ResultReceiver; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.core.app.JobIntentService; -import androidx.documentfile.provider.DocumentFile; - -import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.util.ExportUtils; - -public class ExportService extends JobIntentService { - - private static final String TAG = ExportService.class.getSimpleName(); - - private static final int JOB_ID = 1; - - private static final String EXTRA_RECEIVER = "extra_receiver"; - private static final String EXTRA_EXPORT_TASK = "export_task"; - private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri"; - - public static void enqueue(Context context, ExportServiceResultReceiver receiver, ExportTask exportTask, Uri directoryUri) { - Intent intent = new Intent(context, JobService.class); - intent.putExtra(EXTRA_RECEIVER, receiver); - intent.putExtra(EXTRA_EXPORT_TASK, exportTask); - 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); - ExportTask exportTask = intent.getParcelableExtra(EXTRA_EXPORT_TASK); - Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI); - - // Prepare resultCode and bundle to send to the receiver. - Bundle bundle = new Bundle(); - bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_EXPORT_TASK, exportTask); - - // Build directory file. - DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri); - if (directoryFile == null || !directoryFile.canWrite()) { - bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile); - resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle); - return; - } - - // Export and send result - try { - ExportUtils.exportTrack(this, directoryFile, exportTask); - resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_SUCCESS, bundle); - } catch (Exception e) { - Log.e(TAG, "Export failed: " + e); - e.printStackTrace(); //TODO Remove - - bundle.putString(ExportServiceResultReceiver.EXTRA_EXPORT_ERROR_MESSAGE, e.getMessage()); - resultReceiver.send(ExportServiceResultReceiver.RESULT_CODE_ERROR, bundle); - } - } - - public static 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_EXPORT_TASK = "result_extra_export_task"; - - public static final String EXTRA_EXPORT_ERROR_MESSAGE = "extra_export_error_message"; - - private final Receiver receiver; - - public ExportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) { - super(handler); - this.receiver = receiver; - } - - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - ExportTask exportTask = resultData.getParcelable(RESULT_EXTRA_EXPORT_TASK); - switch (resultCode) { - case RESULT_CODE_SUCCESS -> receiver.onExportSuccess(exportTask); - case RESULT_CODE_ERROR -> receiver.onExportError(exportTask, resultData.getString(EXTRA_EXPORT_ERROR_MESSAGE)); - default -> throw new RuntimeException("Unknown resultCode."); - } - } - - public interface Receiver { - default void onExportSuccess(ExportTask exportTask) { - } - - default void onExportError(ExportTask exportTask, String errorMessage) { - } - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java similarity index 60% rename from src/main/java/de/dennisguse/opentracks/util/ExportUtils.java rename to src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java index 552ee12d6..d9c875aea 100644 --- a/src/main/java/de/dennisguse/opentracks/util/ExportUtils.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java @@ -1,15 +1,21 @@ -package de.dennisguse.opentracks.util; +package de.dennisguse.opentracks.io.file.exporter; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Handler; import android.provider.DocumentsContract; import android.util.Log; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.documentfile.provider.DocumentFile; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; import java.io.FileNotFoundException; import java.io.IOException; @@ -22,36 +28,81 @@ import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFilenameGenerator; -import de.dennisguse.opentracks.io.file.exporter.ExportService; -import de.dennisguse.opentracks.io.file.exporter.ExportService.ExportServiceResultReceiver; -import de.dennisguse.opentracks.io.file.exporter.ExportTask; -import de.dennisguse.opentracks.io.file.exporter.TrackExporter; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.SettingsActivity; +import de.dennisguse.opentracks.util.IntentUtils; public class ExportUtils { private static final String TAG = ExportUtils.class.getSimpleName(); - public static void postWorkoutExport(Context context, Track.Id trackId) { + public static void postWorkoutExport(AppCompatActivity context, Track.Id trackId) { if (PreferencesUtils.shouldInstantExportAfterWorkout()) { TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(); DocumentFile directory = IntentUtils.toDocumentFile(context, PreferencesUtils.getDefaultExportDirectoryUri()); - ExportServiceResultReceiver resultReceiver = new ExportServiceResultReceiver(new Handler(), new ExportServiceResultReceiver.Receiver() { - @Override - public void onExportError(ExportTask unused, String errorMessage) { - Intent intent = new Intent(context, SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, errorMessage); - context.startActivity(intent); - } - }); + WorkManager workManager = WorkManager.getInstance(context); + WorkRequest exportRequest = new OneTimeWorkRequest.Builder(ExportWorker.class) + .setInputData(new Data.Builder() + .putLongArray(ExportWorker.TRACKIDS_KEY, new long[]{trackId.id()}) + .putString(ExportWorker.DIRECTORY_URI_KEY, directory.toString()) + .putString(ExportWorker.TRACKFILEFORMAT_KEY, trackFileFormat.toString()) + .putString(ExportWorker.FILENAME_KEY, null) + .build()) + .build(); - ExportService.enqueue(context, resultReceiver, new ExportTask(null, trackFileFormat, List.of(trackId)), directory.getUri()); + workManager + .getWorkInfoByIdLiveData(exportRequest.getId()) + .observe(context, workInfo -> { + if (workInfo != null) { + if (workInfo.getState() == WorkInfo.State.FAILED) { + Intent intent = new Intent(context, SettingsActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(SettingsActivity.EXTRAS_EXPORT_ERROR_MESSAGE, workInfo.getProgress().getString(ExportWorker.RESULT_EXPORT_ERROR_MESSAGE_KEY)); + context.startActivity(intent); + } + } + }); + + workManager.enqueue(exportRequest); } } + public static void exportTrack(Context context, DocumentFile directory, @Nullable String filenameForMultiple, TrackFileFormat trackFileFormat, List trackIds) { + + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + List tracks = trackIds.stream() + .map(contentProviderUtils::getTrack) + .toList(); + + Uri exportDocumentFileUri; + if (tracks.size() == 1) { + exportDocumentFileUri = getExportDocumentFileUri(context, tracks.get(0), trackFileFormat, directory); + } else { + String filename = TrackFilenameGenerator.format(filenameForMultiple, trackFileFormat); + exportDocumentFileUri = getExportDocumentFileUri(context, filename, trackFileFormat, directory); + } + + if (exportDocumentFileUri == null) { + throw new RuntimeException("Couldn't create document file for export"); + } + + TrackExporter trackExporter = trackFileFormat.createTrackExporter(context, contentProviderUtils); + try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri, "wt")) { + if (!trackExporter.writeTrack(tracks, outputStream)) { + if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) { + throw new RuntimeException("Unable to delete exportDocumentFile"); + } + throw new RuntimeException("Unable to export track"); + } + } catch (FileNotFoundException e) { + throw new RuntimeException("Unable to open exportDocumentFile " + exportDocumentFileUri, e); + } catch (IOException e) { + throw new RuntimeException("Unable to close exportDocumentFile output stream", e); + } + } + + @Deprecated public static void exportTrack(Context context, DocumentFile directory, ExportTask exportTask) { ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); List tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList()); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportWorker.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportWorker.java new file mode 100644 index 000000000..ef0821183 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportWorker.java @@ -0,0 +1,70 @@ +package de.dennisguse.opentracks.io.file.exporter; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.util.Arrays; +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.io.file.TrackFileFormat; + +public class ExportWorker extends Worker { + + private static final String TAG = ExportWorker.class.getSimpleName(); + + static final String TRACKIDS_KEY = "TRACKIDS_KEY"; + static final String DIRECTORY_URI_KEY = "DIRECTORY_URI_KEY"; + static final String TRACKFILEFORMAT_KEY = "TRACKFILEFORMAT_KEY"; + static final String FILENAME_KEY = "FILENAME_KEY"; //optional; only used for multiple tracks into one file + + static final String RESULT_EXPORT_ERROR_MESSAGE_KEY = "EXPORT_ERROR_MESSAGE"; + + private final List trackIds; + + private final DocumentFile directoryFile; + + private final TrackFileFormat trackFileFormat; + + private final String filename; + + public ExportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + + trackIds = Arrays.stream(getInputData().getLongArray(TRACKIDS_KEY)) + .mapToObj(Track.Id::new) + .toList(); + + directoryFile = DocumentFile.fromTreeUri(context, Uri.parse(getInputData().getString(DIRECTORY_URI_KEY))); + + trackFileFormat = TrackFileFormat.valueOf(getInputData().getString(TRACKFILEFORMAT_KEY)); + + filename = getInputData().getString(FILENAME_KEY); + } + + @NonNull + @Override + public Result doWork() { + if (directoryFile == null || !directoryFile.canWrite()) { + return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, getApplicationContext().getString(R.string.export_cannot_write_to_dir) + ": " + directoryFile).build()); + } + + try { + //TODO move method to ExportWorker? + ExportUtils.exportTrack(getApplicationContext(), directoryFile, filename, trackFileFormat, trackIds); + return Result.success(); + } catch (Exception e) { + Log.e(TAG, "Export failed: " + e); + + return Result.failure(new Data.Builder().putString(RESULT_EXPORT_ERROR_MESSAGE_KEY, e.getMessage()).build()); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/publicapi/StopRecording.java b/src/main/java/de/dennisguse/opentracks/publicapi/StopRecording.java index 7e0910fe4..3432576ad 100644 --- a/src/main/java/de/dennisguse/opentracks/publicapi/StopRecording.java +++ b/src/main/java/de/dennisguse/opentracks/publicapi/StopRecording.java @@ -1,9 +1,9 @@ package de.dennisguse.opentracks.publicapi; import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.io.file.exporter.ExportUtils; import de.dennisguse.opentracks.services.RecordingData; import de.dennisguse.opentracks.services.TrackRecordingService; -import de.dennisguse.opentracks.util.ExportUtils; public class StopRecording extends AbstractAPIActivity {