From 4baaff837412d6368573a8b072ce533f69b26041 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 6 Aug 2023 11:41:40 +0200 Subject: [PATCH] TrackDeleteService can be a JobIntentService (rather than ForegroundService). Android 14 requires ForegroundService to have a reason. Fixes #1657. --- src/main/AndroidManifest.xml | 4 +- .../AbstractTrackDeleteActivity.java | 48 +----- .../opentracks/TrackListActivity.java | 2 +- .../opentracks/TrackRecordedActivity.java | 14 +- .../opentracks/TrackStoppedActivity.java | 2 +- .../io/file/exporter/ExportService.java | 4 - .../services/TrackDeleteService.java | 151 +++++------------- .../TrackDeleteServiceConnection.java | 66 -------- 8 files changed, 44 insertions(+), 247 deletions(-) delete mode 100644 src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index a87569e9a..79e391421 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -410,10 +410,8 @@ limitations under the License. + android:permission="android.permission.BIND_JOB_SERVICE"/> diff --git a/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java b/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java index cf76d7eee..2bb463335 100644 --- a/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java +++ b/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java @@ -16,6 +16,7 @@ package de.dennisguse.opentracks; +import android.os.Handler; import android.widget.Toast; import java.util.ArrayList; @@ -24,7 +25,6 @@ import java.util.stream.Collectors; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.services.TrackDeleteService; -import de.dennisguse.opentracks.services.TrackDeleteServiceConnection; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment.ConfirmDeleteCaller; @@ -37,31 +37,8 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm * @author Jimmy Shih */ //TODO Check if this class is still such a good idea; inheritance might limit maintainability -public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener { +public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteService.TrackDeleteResultReceiver.Receiver { - private TrackDeleteServiceConnection trackDeleteServiceConnection; - - @Override - protected void onStart() { - super.onStart(); - trackDeleteServiceConnection = new TrackDeleteServiceConnection(this); - trackDeleteServiceConnection.bind(this); - } - - @Override - protected void onStop() { - super.onStop(); - if (trackDeleteServiceConnection != null) { - trackDeleteServiceConnection.unbind(this); - trackDeleteServiceConnection = null; - } - } - - /** - * Delete tracks. - * - * @param trackIds the track ids - */ protected void deleteTracks(Track.Id... trackIds) { ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds); } @@ -77,29 +54,10 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple Toast.makeText(this, getString(R.string.track_delete_not_recording), Toast.LENGTH_LONG).show(); } - trackDeleteServiceConnection = new TrackDeleteServiceConnection(this); - trackDeleteServiceConnection.startAndBind(this, trackIdList); - } - - /** - * Called every time a track is deleted. - */ - protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) { - if (trackDeleteServiceConnection != null) { - trackDeleteServiceConnection.unbind(this); - trackDeleteServiceConnection = null; - onDeleteFinished(); - } + TrackDeleteService.enqueue(this, new TrackDeleteService.TrackDeleteResultReceiver(new Handler(), this), trackIdList); } protected abstract void onDeleteConfirmed(); - protected abstract void onDeleteFinished(); - protected abstract Track.Id getRecordingTrackId(); - - @Override - public void connected(TrackDeleteService service) { - service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, this::onTrackDeleteStatus); - } } diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index 53372bc90..584b6f408 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -422,7 +422,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co } @Override - protected void onDeleteFinished() { + public void onDeleteFinished() { // Do nothing } diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java index cba70be8b..f47e6b7c8 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java @@ -40,7 +40,6 @@ import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.databinding.TrackRecordedBinding; import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment; import de.dennisguse.opentracks.services.RecordingStatus; -import de.dennisguse.opentracks.services.TrackDeleteService; import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.settings.SettingsActivity; @@ -230,22 +229,13 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement return recordingStatus.getTrackId(); } - @Override - protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) { - super.onTrackDeleteStatus(deletionFinishedStatus); - if (deletionFinishedStatus.isDeleted(trackId)) { - runOnUiThread(this::finish); - } - } - @Override protected void onDeleteConfirmed() { runOnUiThread(this::finish); } - @Override - protected void onDeleteFinished() { - // Do nothing + public void onDeleteFinished() { + runOnUiThread(this::finish); } /** diff --git a/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java index 14becbb88..03ddd5fc4 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java @@ -158,7 +158,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements } @Override - protected void onDeleteFinished() { + public void onDeleteFinished() { finish(); } 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 index b97e6e1bf..51538769b 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportService.java @@ -59,10 +59,6 @@ public class ExportService extends JobIntentService { resultReceiver.send(resultCode, bundle); } - /** - * 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 static class ExportServiceResultReceiver extends ResultReceiver { public static final int RESULT_CODE_SUCCESS = 1; diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java index 590846b21..3136ae731 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java @@ -1,146 +1,67 @@ package de.dennisguse.opentracks.services; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; +import android.app.job.JobService; import android.content.Context; import android.content.Intent; -import android.os.Build; -import android.os.IBinder; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; +import androidx.core.app.JobIntentService; import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Track; -public class TrackDeleteService extends Service { +public class TrackDeleteService extends JobIntentService { - private static final String CHANNEL_ID = TrackDeleteService.class.getSimpleName(); - private static final int NOTIFICATION_ID = 1; + private static final int JOB_ID = 3; - static final String EXTRA_TRACK_IDS = "extra_track_ids"; + private static final String EXTRA_RECEIVER = "extra_receiver"; - private final Binder binder = new Binder(); - private ExecutorService serviceExecutor; - private MutableLiveData deleteResultObservable; - private NotificationManager notificationManager; - private NotificationCompat.Builder notificationBuilder; + private static final String EXTRA_TRACK_IDS = "extra_track_ids"; - @Override - public void onCreate() { - serviceExecutor = Executors.newSingleThreadExecutor(); - deleteResultObservable = new MutableLiveData<>(); + public static void enqueue(Context context, TrackDeleteResultReceiver receiver, ArrayList toBeDeleted) { + Intent intent = new Intent(context, JobService.class); + intent.putExtra(EXTRA_RECEIVER, receiver); + intent.putParcelableArrayListExtra(EXTRA_TRACK_IDS, toBeDeleted); + enqueueWork(context, TrackDeleteService.class, JOB_ID, intent); } @Override - public void onDestroy() { - super.onDestroy(); - if (serviceExecutor != null) { - serviceExecutor.shutdownNow(); - serviceExecutor = null; - } - deleteResultObservable = null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { + protected void onHandleWork(@NonNull Intent intent) { + ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER); ArrayList trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS); - createAndShowNotification(trackIds.size()); - deleteTracks(trackIds); - return START_NOT_STICKY; + + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); + contentProviderUtils.deleteTracks(this, trackIds); + + resultReceiver.send(TrackDeleteResultReceiver.RESULT_CODE_SUCCESS, new Bundle()); } - private void deleteTracks(@NonNull List trackIds) { - serviceExecutor.execute(() -> { - ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); - contentProviderUtils.deleteTracks(this, trackIds); - sendResult(trackIds); - stopSelf(); - }); - } + public static class TrackDeleteResultReceiver extends ResultReceiver { - private void sendResult(List trackIds) { - if (deleteResultObservable != null) { - deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds)); - } - } + public static final int RESULT_CODE_SUCCESS = 1; - @Override - public IBinder onBind(Intent intent) { - return binder; - } + private final Receiver receiver; - public LiveData getDeletingStatusObservable() { - return deleteResultObservable; - } - - /** - * Starts and shows the notification. - * - * @param tracksToDelete number of tracks to be deleted. - */ - private void createAndShowNotification(int tracksToDelete) { - notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, this.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - notificationChannel.setAllowBubbles(true); - } - - notificationManager.createNotificationChannel(notificationChannel); + public TrackDeleteResultReceiver(Handler handler, @NonNull Receiver receiver) { + super(handler); + this.receiver = receiver; } - notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID); - notificationBuilder - .setContentTitle(this.getString(R.string.track_delete_progress_message)) - .setContentText(this.getString(R.string.track_delete_number_of_tracks, tracksToDelete)) - .setSmallIcon(R.drawable.ic_logo_color_24dp) - .setProgress(0, 0, true); - - startForeground(NOTIFICATION_ID, notificationBuilder.build()); - } - - public class Binder extends android.os.Binder { - - private Binder() { - super(); - } - - public TrackDeleteService getService() { - return TrackDeleteService.this; - } - } - - public static class DeletionFinishedStatus { - private final List trackIds; - - /** - * @param trackIds List of deleted Track.Ids. - */ - private DeletionFinishedStatus(@Nullable List trackIds) { - this.trackIds = trackIds; - } - - public boolean isDeleted(Track.Id trackId) { - return this.trackIds != null && this.trackIds.contains(trackId); - } - - @NonNull @Override - public String toString() { - return "DeleteStatus{" + - "trackIds=" + trackIds + - '}'; + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case RESULT_CODE_SUCCESS -> receiver.onDeleteFinished(); + default -> throw new RuntimeException("Unknown resultCode."); + } + } + + public interface Receiver { + void onDeleteFinished(); } } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java deleted file mode 100644 index fbbb28b85..000000000 --- a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.dennisguse.opentracks.services; - -import static de.dennisguse.opentracks.services.TrackDeleteService.EXTRA_TRACK_IDS; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; - -import de.dennisguse.opentracks.BuildConfig; -import de.dennisguse.opentracks.data.models.Track; - -public class TrackDeleteServiceConnection implements ServiceConnection { - - final private Listener listener; - private TrackDeleteService trackDeleteService; - - public TrackDeleteServiceConnection(@NonNull Listener listener) { - this.listener = listener; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - trackDeleteService = ((TrackDeleteService.Binder) service).getService(); - listener.connected(trackDeleteService); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - trackDeleteService = null; - } - - public void startAndBind(Context context, ArrayList trackIds) { - if (trackDeleteService != null) { - return; - } - - Intent intent = new Intent(context, TrackDeleteService.class) - .putParcelableArrayListExtra(EXTRA_TRACK_IDS, trackIds); - context.startService(intent); - - bind(context); - } - - public void bind(Context context) { - if (trackDeleteService != null) { - return; - } - - context.bindService(new Intent(context, TrackDeleteService.class), this, BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0); - } - - public void unbind(Context context) { - context.unbindService(this); - trackDeleteService = null; - } - - public interface Listener { - void connected(@NonNull TrackDeleteService service); - } -}