Track deletion is handled by a service that shows a progress bar on the notification bar. Fixes #735.

This commit is contained in:
Román Martínez
2021-05-10 18:23:00 +02:00
parent 6b0ec72b9b
commit 93f203bcf1
11 changed files with 601 additions and 142 deletions
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import androidx.annotation.Nullable;
import java.util.ArrayList;
@@ -26,33 +24,38 @@ import java.util.Arrays;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
* <p>
* - share track <br>
* - delete tracks <br>
*
* @author Jimmy Shih
*/
//TODO It is actually not a List; we need a better name for this class
//TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
private static final String TAG = AbstractListActivity.class.getSimpleName();
private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
private TrackDeleteServiceConnection trackDeleteServiceConnection;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
onTrackDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
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;
}
}
@@ -69,6 +72,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false;
onDeleteConfirmed();
for (Track.Id trackId : trackIds) {
if (trackId.equals(getRecordingTrackId())) {
stopRecording = true;
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this);
}
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
startActivityForResult(intent, DELETE_REQUEST_CODE);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
}
/**
@@ -94,12 +98,28 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
}
/**
* Called after {@link TrackDeleteActivity} returns its result.
* Called every time a track is deleted.
*/
abstract protected void onTrackDeleted();
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
}
}
protected abstract void onDeleteConfirmed();
@Nullable
protected Track.Id getRecordingTrackId() {
return null;
}
@Override
public void connected() {
TrackDeleteService service = trackDeleteServiceConnection.getServiceIfBound();
if (service == null) {
return;
}
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, status -> onTrackDeleteStatus(status));
}
}
@@ -1,101 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.View;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* An activity for deleting tracks.
*
* @author Jimmy Shih
*/
public class TrackDeleteActivity extends AbstractActivity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private List<Track.Id> trackIds;
private Thread deleteThread;
private TrackDeleteBinding viewBinding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
Intent intent = getIntent();
trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
deleteThread = new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (Thread.interrupted()) {
return;
}
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
});
deleteThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
protected void onStart() {
super.onStart();
if (deleteThread.getState() == Thread.State.TERMINATED) {
onAsyncTaskCompleted();
}
}
@Override
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected View getRootView() {
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
public void onAsyncTaskCompleted() {
viewBinding.progressbar.setVisibility(View.INVISIBLE);
setResult(RESULT_OK);
finish();
}
}
@@ -71,10 +71,12 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
private static final String TAG = TrackListActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
// The following are set in onCreate
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -398,7 +400,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
protected void onTrackDeleted() {
protected void onDeleteConfirmed() {
// Do nothing
}
@@ -459,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return true;
}
if (itemId == R.id.list_context_menu_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
@@ -471,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
deleteTracks(trackIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -56,7 +57,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* @author Rodrigo Damazio
*/
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
public class TrackRecordedActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
@@ -248,9 +249,17 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return recordingStatus.getTrackId();
}
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
Log.e("probando", "trackId: " + trackId);
if (deleteStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@Override
protected void onTrackDeleted() {
protected void onDeleteConfirmed() {
runOnUiThread(this::finish);
}
@@ -0,0 +1,171 @@
package de.dennisguse.opentracks.services;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
public class TrackDeleteService extends Service {
private static final String CHANNEL_ID = TrackDeleteService.class.getSimpleName();
private static final int NOTIFICATION_ID = 1;
static final String EXTRA_TRACK_IDS = "extra_track_ids";
private final Binder binder = new Binder();
private ExecutorService serviceExecutor;
private MutableLiveData<DeleteStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@Override
public void onCreate() {
serviceExecutor = Executors.newSingleThreadExecutor();
deleteResultObservable = new MutableLiveData<>();
}
@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) {
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
createAndShowNotification(trackIds.size());
deleteTracks(trackIds);
return START_NOT_STICKY;
}
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
sendResult(null,0, trackIds.size());
serviceExecutor.execute(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
for (int i = 0; i < trackIds.size(); i++) {
updateNotification(i + 1, trackIds.size());
contentProviderUtils.deleteTrack(this, trackIds.get(i));
sendResult(trackIds.get(i), i + 1, trackIds.size());
}
stopSelf();
});
}
private void sendResult(Track.Id trackId, int deletes, int total) {
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public LiveData<DeleteStatus> 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);
}
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
notificationBuilder
.setContentTitle(this.getString(R.string.track_delete_progress_message))
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setProgress(tracksToDelete, 0, false);
startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
/**
* Updates notification progress.
*
* @param progress number of tracks already deleted.
* @param total total of tracks to be deleted.
*/
private void updateNotification(int progress, int total) {
notificationBuilder.setProgress(total, progress, false);
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
public class Binder extends android.os.Binder {
private Binder() {
super();
}
public TrackDeleteService getService() {
return TrackDeleteService.this;
}
}
public static class DeleteStatus {
private int progress;
private int max;
private Track.Id trackId;
/**
* @param trackId Track.Id just deleted if any.
* @param progress number of deletes.
* @param max total of deletes to be done.
*/
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
this.trackId = trackId;
this.progress = progress;
this.max = max;
}
public boolean isFinished() {
return progress == max;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackId != null && this.trackId.equals(trackId);
}
@Override
public String toString() {
return "DeleteStatus{" +
"number of deletes=" + progress +
", total=" + max +
'}';
}
}
}
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.services;
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.content.data.Track;
import static de.dennisguse.opentracks.services.TrackDeleteService.EXTRA_TRACK_IDS;
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();
}
@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 TrackDeleteService getServiceIfBound() {
return trackDeleteService;
}
public interface Listener {
void connected();
}
}