From 93f203bcf1625a0afda2cd17dd4fd69390ccff7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Mon, 10 May 2021 18:23:00 +0200 Subject: [PATCH] Track deletion is handled by a service that shows a progress bar on the notification bar. Fixes #735. --- .../opentracks/EspressoDeleteTrackTest.java | 285 ++++++++++++++++++ .../dennisguse/opentracks/EspressoUITest.java | 10 + src/main/AndroidManifest.xml | 11 +- ....java => AbstractTrackDeleteActivity.java} | 60 ++-- .../opentracks/TrackDeleteActivity.java | 101 ------- .../opentracks/TrackListActivity.java | 8 +- .../opentracks/TrackRecordedActivity.java | 13 +- .../services/TrackDeleteService.java | 171 +++++++++++ .../TrackDeleteServiceConnection.java | 70 +++++ src/main/res/layout/track_delete.xml | 12 - src/main/res/values/strings.xml | 2 + 11 files changed, 601 insertions(+), 142 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java rename src/main/java/de/dennisguse/opentracks/{AbstractListActivity.java => AbstractTrackDeleteActivity.java} (57%) delete mode 100644 src/main/java/de/dennisguse/opentracks/TrackDeleteActivity.java create mode 100644 src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java create mode 100644 src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java delete mode 100644 src/main/res/layout/track_delete.xml diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java new file mode 100644 index 000000000..4efb59a93 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java @@ -0,0 +1,285 @@ +package de.dennisguse.opentracks; + + +import android.app.ActivityManager; +import android.app.Instrumentation; +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.ListView; + +import androidx.test.espresso.Espresso; +import androidx.test.espresso.IdlingResource; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.ViewInteraction; +import androidx.test.espresso.action.MotionEvents; +import androidx.test.espresso.assertion.ViewAssertions; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import de.dennisguse.opentracks.services.TrackDeleteService; + +import static androidx.test.espresso.Espresso.onData; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.Espresso.pressBack; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.scrollTo; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anything; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class EspressoDeleteTrackTest { + + @Rule + public ActivityScenarioRule mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); + + @Rule + public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION"); + + private MyIdlingResource idlingResource; + + @Before + public void registerIntentServiceIdlingResource() { + Instrumentation instrumentation + = InstrumentationRegistry.getInstrumentation(); + idlingResource = new MyIdlingResource(instrumentation.getTargetContext()); + Espresso.registerIdlingResources(idlingResource); + } + + @After + public void unregisterIntentServiceIdlingResource() { + Espresso.unregisterIdlingResources(idlingResource); + } + + @Test + public void espressoDeleteTrackTest() { + int countBefore; + + { + // TrackListActivity: start recording + ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record)); + trackControllerRecordButton.perform(click()); + } + + { + // TrackRecordingActivity + ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record)); + ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop)); + + // wait; stay recording + trackControllerRecordButton.perform(waitFor(5000)); + + // stop; + trackControllerStopButton.perform(veryLongTouch(1600)); + } + + // back + pressBack(); + + // get number of items before deleting a track + countBefore = numberOfItemsListView(); + + // select track + onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000)); + + // open menu and delete selected track + ViewInteraction overflowMenuButton = onView( + allOf(withContentDescription("More options"), + childAtPosition( + childAtPosition( + withId(R.id.toolbar), + 2), + 2), + isDisplayed())); + overflowMenuButton.perform(click()); + + ViewInteraction appCompatTextView = onView( + allOf(withId(R.id.title), withText("Delete"), + childAtPosition( + childAtPosition( + withId(R.id.content), + 0), + 0), + isDisplayed())); + appCompatTextView.perform(click()); + + ViewInteraction appCompatButton = onView( + allOf(withId(android.R.id.button1), withText("Yes"), + childAtPosition( + childAtPosition( + withId(R.id.buttonPanel), + 0), + 3))); + appCompatButton.perform(scrollTo(), click()); + + // check number of items after delete + onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1))); + } + + private static Matcher childAtPosition( + final Matcher parentMatcher, final int position) { + + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("Child at position " + position + " in parent "); + parentMatcher.describeTo(description); + } + + @Override + public boolean matchesSafely(View view) { + ViewParent parent = view.getParent(); + return parent instanceof ViewGroup && parentMatcher.matches(parent) + && view.equals(((ViewGroup) parent).getChildAt(position)); + } + }; + } + + public static Matcher withListSize (final int size) { + return new TypeSafeMatcher () { + @Override public boolean matchesSafely (final View view) { + return ((ListView) view).getCount () == size; + } + + @Override public void describeTo (final Description description) { + description.appendText ("ListView should have " + size + " items"); + } + }; + } + + private static ViewAction waitFor(final long duration_ms) { + return new ViewAction() { + + @Override + public String getDescription() { + return "Wait for milliseconds."; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, final View view) { + uiController.loopMainThreadForAtLeast(duration_ms); + } + }; + } + + private static ViewAction veryLongTouch(final int duration_ms) { + return new ViewAction() { + @Override + public String getDescription() { + return "Perform long touch."; + } + + @Override + public Matcher getConstraints() { + return isDisplayed(); + } + + @Override + public void perform(UiController uiController, final View view) { + // Get view absolute position + int[] location = new int[2]; + view.getLocationOnScreen(location); + + // Offset coordinates by view position + float[] coordinates = new float[]{location[0] + 1, location[1] + 1}; + + // Send down event, pause, and send up + MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down; + uiController.loopMainThreadForAtLeast(duration_ms); + MotionEvents.sendUp(uiController, down, coordinates); + } + }; + } + + private int numberOfItemsListView() { + final int[] counts = new int[1]; + onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher() { + @Override + public boolean matchesSafely(View view) { + ListView listView = (ListView) view; + + counts[0] = listView.getCount(); + + return true; + } + + @Override + public void describeTo(Description description) { + + } + })); + + return counts[0]; + } + + + private class MyIdlingResource implements IdlingResource { + + private Context context; + private ResourceCallback resourceCallback; + + public MyIdlingResource(Context context) { + this.context = context; + } + + @Override + public String getName() { + return MyIdlingResource.class.getName(); + } + + @Override + public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) { + this.resourceCallback = resourceCallback; + } + + @Override + public boolean isIdleNow() { + boolean idle = !isDeleteServiceRunning(); + if (idle && resourceCallback != null) { + resourceCallback.onTransitionToIdle(); + } + return idle; + } + + private boolean isDeleteServiceRunning() { + ActivityManager manager = + (ActivityManager) context.getSystemService( + Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo info : + manager.getRunningServices(Integer.MAX_VALUE)) { + if (TrackDeleteService.class.getName().equals( + info.service.getClassName())) { + return true; + } + } + return false; + } + } +} diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java index a2453e969..2876c33e2 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoUITest.java @@ -19,10 +19,13 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.Matchers.anything; @LargeTest @RunWith(AndroidJUnit4.class) @@ -97,6 +100,13 @@ public class EspressoUITest { } } + @LargeTest + @Test + public void selectAndDeleteTrack() { + onView(withId(R.id.track_list)).check(matches(isDisplayed())); + onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(2000)); + } + private static ViewAction veryLongTouch(final int duration_ms) { return new ViewAction() { @Override diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 41f9d09b0..4acc28fea 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -59,10 +59,6 @@ limitations under the License. - - @@ -350,6 +346,13 @@ limitations under the License. android:name=".io.file.importer.ImportService" android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE" /> + + diff --git a/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java b/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java similarity index 57% rename from src/main/java/de/dennisguse/opentracks/AbstractListActivity.java rename to src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java index f0d7066ea..b86f42617 100644 --- a/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/AbstractTrackDeleteActivity.java @@ -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}: *

- * - share track
* - delete tracks
* * @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)); + } } diff --git a/src/main/java/de/dennisguse/opentracks/TrackDeleteActivity.java b/src/main/java/de/dennisguse/opentracks/TrackDeleteActivity.java deleted file mode 100644 index 5ed156b23..000000000 --- a/src/main/java/de/dennisguse/opentracks/TrackDeleteActivity.java +++ /dev/null @@ -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 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(); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index 60be0b8a7..83bdee7b3 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -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); diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java index 3b3388229..8137f670b 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java @@ -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); } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java new file mode 100644 index 000000000..f8f60bcca --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteService.java @@ -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 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 trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS); + createAndShowNotification(trackIds.size()); + deleteTracks(trackIds); + return START_NOT_STICKY; + } + + private void deleteTracks(@NonNull ArrayList 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 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 + + '}'; + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java new file mode 100644 index 000000000..0efd27c19 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/services/TrackDeleteServiceConnection.java @@ -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 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(); + } +} diff --git a/src/main/res/layout/track_delete.xml b/src/main/res/layout/track_delete.xml deleted file mode 100644 index 235dc64fe..000000000 --- a/src/main/res/layout/track_delete.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index faf1fb114..d75a025fc 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -352,6 +352,7 @@ limitations under the License. Resume track Recording service + Delete service OpenTracks requires permission to use GPS. @@ -475,6 +476,7 @@ limitations under the License. Delete track? The selected tracks and their markers will be permanently deleted from the device. Deleting… + %1$d/%2$d Chart Stats