Migrate ExportService to WorkManager.

Part of #1315.
This commit is contained in:
Dennis Guse
2025-06-28 17:11:27 +02:00
parent 6f97399aeb
commit 02a77e5ce7
7 changed files with 177 additions and 139 deletions
-5
View File
@@ -395,11 +395,6 @@ limitations under the License.
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
<service
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"
@@ -16,10 +16,10 @@ import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.exporter.ExportUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -27,6 +27,11 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
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.util.ArrayList;
import java.util.List;
@@ -41,7 +46,6 @@ import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -56,7 +60,8 @@ import de.dennisguse.opentracks.util.FileUtils;
* So, for this check actually a different file name might be used than in the ExportService.
* * Saved state as an object instead of individual values.
*/
public class ExportActivity extends AppCompatActivity implements ExportService.ExportServiceResultReceiver.Receiver {
//TODO Make an AbstractActivity
public class ExportActivity extends AppCompatActivity {
private static final String TAG = ExportActivity.class.getSimpleName();
@@ -84,8 +89,6 @@ public class ExportActivity extends AppCompatActivity implements ExportService.E
private TrackFileFormat trackFileFormat;
private Uri directoryUri;
private ExportService.ExportServiceResultReceiver resultReceiver;
private List<String> 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;
@@ -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) {
}
}
}
}
@@ -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<Track.Id> trackIds) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Track> 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<Track> tracks = exportTask.getTrackIds().stream().map(contentProviderUtils::getTrack).collect(Collectors.toList());
@@ -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<Track.Id> 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());
}
}
}
@@ -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 {