TrackDeleteService can be a JobIntentService (rather than ForegroundService).

Android 14 requires ForegroundService to have a reason.

Fixes  #1657.
This commit is contained in:
Dennis Guse
2023-08-06 11:41:40 +02:00
parent 1ed0a11c2f
commit 4baaff8374
8 changed files with 44 additions and 247 deletions
+1 -3
View File
@@ -410,10 +410,8 @@ limitations under the License.
<service <service
android:name=".services.TrackDeleteService" android:name=".services.TrackDeleteService"
android:enabled="true"
android:exported="false" android:exported="false"
android:icon="@drawable/ic_logo_color_24dp" android:permission="android.permission.BIND_JOB_SERVICE"/>
android:label="@string/delete_service" />
</application> </application>
<queries> <queries>
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.os.Handler;
import android.widget.Toast; import android.widget.Toast;
import java.util.ArrayList; import java.util.ArrayList;
@@ -24,7 +25,6 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackDeleteService; 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;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment.ConfirmDeleteCaller; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
@@ -37,31 +37,8 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
* @author Jimmy Shih * @author Jimmy Shih
*/ */
//TODO Check if this class is still such a good idea; inheritance might limit maintainability //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) { protected void deleteTracks(Track.Id... trackIds) {
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), 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(); Toast.makeText(this, getString(R.string.track_delete_not_recording), Toast.LENGTH_LONG).show();
} }
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this); TrackDeleteService.enqueue(this, new TrackDeleteService.TrackDeleteResultReceiver(new Handler(), this), trackIdList);
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();
}
} }
protected abstract void onDeleteConfirmed(); protected abstract void onDeleteConfirmed();
protected abstract void onDeleteFinished();
protected abstract Track.Id getRecordingTrackId(); protected abstract Track.Id getRecordingTrackId();
@Override
public void connected(TrackDeleteService service) {
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, this::onTrackDeleteStatus);
}
} }
@@ -422,7 +422,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
} }
@Override @Override
protected void onDeleteFinished() { public void onDeleteFinished() {
// Do nothing // Do nothing
} }
@@ -40,7 +40,6 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding; import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -230,22 +229,13 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
return recordingStatus.getTrackId(); return recordingStatus.getTrackId();
} }
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
super.onTrackDeleteStatus(deletionFinishedStatus);
if (deletionFinishedStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@Override @Override
protected void onDeleteConfirmed() { protected void onDeleteConfirmed() {
runOnUiThread(this::finish); runOnUiThread(this::finish);
} }
@Override public void onDeleteFinished() {
protected void onDeleteFinished() { runOnUiThread(this::finish);
// Do nothing
} }
/** /**
@@ -158,7 +158,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
} }
@Override @Override
protected void onDeleteFinished() { public void onDeleteFinished() {
finish(); finish();
} }
@@ -59,10 +59,6 @@ public class ExportService extends JobIntentService {
resultReceiver.send(resultCode, bundle); resultReceiver.send(resultCode, bundle);
} }
/**
* 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 static class ExportServiceResultReceiver extends ResultReceiver { public static class ExportServiceResultReceiver extends ResultReceiver {
public static final int RESULT_CODE_SUCCESS = 1; public static final int RESULT_CODE_SUCCESS = 1;
@@ -1,146 +1,67 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.app.NotificationChannel; import android.app.job.JobService;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Bundle;
import android.os.IBinder; import android.os.Handler;
import android.os.ResultReceiver;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.core.app.JobIntentService;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList; 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.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; 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 JOB_ID = 3;
private static final int NOTIFICATION_ID = 1;
static final String EXTRA_TRACK_IDS = "extra_track_ids"; private static final String EXTRA_RECEIVER = "extra_receiver";
private final Binder binder = new Binder(); private static final String EXTRA_TRACK_IDS = "extra_track_ids";
private ExecutorService serviceExecutor;
private MutableLiveData<DeletionFinishedStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@Override public static void enqueue(Context context, TrackDeleteResultReceiver receiver, ArrayList<Track.Id> toBeDeleted) {
public void onCreate() { Intent intent = new Intent(context, JobService.class);
serviceExecutor = Executors.newSingleThreadExecutor(); intent.putExtra(EXTRA_RECEIVER, receiver);
deleteResultObservable = new MutableLiveData<>(); intent.putParcelableArrayListExtra(EXTRA_TRACK_IDS, toBeDeleted);
enqueueWork(context, TrackDeleteService.class, JOB_ID, intent);
} }
@Override @Override
public void onDestroy() { protected void onHandleWork(@NonNull Intent intent) {
super.onDestroy(); ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
if (serviceExecutor != null) {
serviceExecutor.shutdownNow();
serviceExecutor = null;
}
deleteResultObservable = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS); ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
createAndShowNotification(trackIds.size());
deleteTracks(trackIds); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
return START_NOT_STICKY; contentProviderUtils.deleteTracks(this, trackIds);
resultReceiver.send(TrackDeleteResultReceiver.RESULT_CODE_SUCCESS, new Bundle());
} }
private void deleteTracks(@NonNull List<Track.Id> trackIds) { public static class TrackDeleteResultReceiver extends ResultReceiver {
serviceExecutor.execute(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
contentProviderUtils.deleteTracks(this, trackIds);
sendResult(trackIds);
stopSelf();
});
}
private void sendResult(List<Track.Id> trackIds) { public static final int RESULT_CODE_SUCCESS = 1;
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds));
}
}
@Override private final Receiver receiver;
public IBinder onBind(Intent intent) {
return binder;
}
public LiveData<DeletionFinishedStatus> getDeletingStatusObservable() { public TrackDeleteResultReceiver(Handler handler, @NonNull Receiver receiver) {
return deleteResultObservable; super(handler);
} this.receiver = receiver;
/**
* 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);
} }
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<Track.Id> trackIds;
/**
* @param trackIds List of deleted Track.Ids.
*/
private DeletionFinishedStatus(@Nullable List<Track.Id> trackIds) {
this.trackIds = trackIds;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackIds != null && this.trackIds.contains(trackId);
}
@NonNull
@Override @Override
public String toString() { protected void onReceiveResult(int resultCode, Bundle resultData) {
return "DeleteStatus{" + switch (resultCode) {
"trackIds=" + trackIds + case RESULT_CODE_SUCCESS -> receiver.onDeleteFinished();
'}'; default -> throw new RuntimeException("Unknown resultCode.");
}
}
public interface Receiver {
void onDeleteFinished();
} }
} }
} }
@@ -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<Track.Id> 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);
}
}