forked from upstream-mirrors/OpenTracks
Track deletion is handled by a service that shows a progress bar on the notification bar. Fixes #735.
This commit is contained in:
@@ -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<TrackListActivity> 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<View> childAtPosition(
|
||||||
|
final Matcher<View> parentMatcher, final int position) {
|
||||||
|
|
||||||
|
return new TypeSafeMatcher<View>() {
|
||||||
|
@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<View> withListSize (final int size) {
|
||||||
|
return new TypeSafeMatcher<View> () {
|
||||||
|
@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<View> 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<View> 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<View>() {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,13 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onData;
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
import static androidx.test.espresso.action.ViewActions.click;
|
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.isDisplayed;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
|
import static org.hamcrest.Matchers.anything;
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@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) {
|
private static ViewAction veryLongTouch(final int duration_ms) {
|
||||||
return new ViewAction() {
|
return new ViewAction() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ limitations under the License.
|
|||||||
|
|
||||||
<activity android:name=".AboutActivity" />
|
<activity android:name=".AboutActivity" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".TrackDeleteActivity"
|
|
||||||
android:theme="@style/TrackDeleteActivity" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HelpActivity"
|
android:name=".HelpActivity"
|
||||||
android:label="@string/menu_help" />
|
android:label="@string/menu_help" />
|
||||||
@@ -350,6 +346,13 @@ limitations under the License.
|
|||||||
android:name=".io.file.importer.ImportService"
|
android:name=".io.file.importer.ImportService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.TrackDeleteService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:icon="@drawable/ic_logo_color_24dp"
|
||||||
|
android:label="@string/delete_service" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
|
|||||||
+40
-20
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -26,33 +24,38 @@ import java.util.Arrays;
|
|||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
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.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class for the following common tasks across
|
* An abstract class for the following common tasks across
|
||||||
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
* {@link TrackListActivity} and {@link TrackRecordedActivity}:
|
||||||
* <p>
|
* <p>
|
||||||
* - share track <br>
|
|
||||||
* - delete tracks <br>
|
* - delete tracks <br>
|
||||||
*
|
*
|
||||||
* @author Jimmy Shih
|
* @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
|
//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 TrackDeleteServiceConnection trackDeleteServiceConnection;
|
||||||
private static final int DELETE_REQUEST_CODE = 3;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onStart() {
|
||||||
if (requestCode == DELETE_REQUEST_CODE) {
|
super.onStart();
|
||||||
onTrackDeleted();
|
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||||
} else {
|
trackDeleteServiceConnection.bind(this);
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
}
|
||||||
|
|
||||||
|
@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) {
|
public void onConfirmDeleteDone(Track.Id... trackIds) {
|
||||||
boolean stopRecording = false;
|
boolean stopRecording = false;
|
||||||
|
|
||||||
|
onDeleteConfirmed();
|
||||||
|
|
||||||
for (Track.Id trackId : trackIds) {
|
for (Track.Id trackId : trackIds) {
|
||||||
if (trackId.equals(getRecordingTrackId())) {
|
if (trackId.equals(getRecordingTrackId())) {
|
||||||
stopRecording = true;
|
stopRecording = true;
|
||||||
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
|||||||
if (stopRecording) {
|
if (stopRecording) {
|
||||||
getTrackRecordingServiceConnection().stopRecording(this);
|
getTrackRecordingServiceConnection().stopRecording(this);
|
||||||
}
|
}
|
||||||
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
|
|
||||||
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds)));
|
trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
|
||||||
startActivityForResult(intent, DELETE_REQUEST_CODE);
|
trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
@Nullable
|
||||||
protected Track.Id getRecordingTrackId() {
|
protected Track.Id getRecordingTrackId() {
|
||||||
return null;
|
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
|
* @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();
|
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
protected static final int GPS_REQUEST_CODE = 6;
|
||||||
|
|
||||||
// The following are set in onCreate
|
// The following are set in onCreate
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
@@ -398,7 +400,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTrackDeleted() {
|
protected void onDeleteConfirmed() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (itemId == R.id.list_context_menu_edit) {
|
if (itemId == R.id.list_context_menu_edit) {
|
||||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||||
@@ -471,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
deleteTracks(trackIds);
|
deleteTracks(trackIds);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemId == R.id.list_context_menu_select_all) {
|
if (itemId == R.id.list_context_menu_select_all) {
|
||||||
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
|
||||||
viewBinding.trackList.setItemChecked(i, true);
|
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.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||||
|
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;
|
||||||
@@ -56,7 +57,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
|||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
|
//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();
|
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
||||||
|
|
||||||
@@ -248,9 +249,17 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
|||||||
return recordingStatus.getTrackId();
|
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
|
@Override
|
||||||
protected void onTrackDeleted() {
|
protected void onDeleteConfirmed() {
|
||||||
runOnUiThread(this::finish);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progressbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minHeight="100dp"
|
|
||||||
android:minWidth="100dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -352,6 +352,7 @@ limitations under the License.
|
|||||||
<string name="menu_resume_track">Resume track</string>
|
<string name="menu_resume_track">Resume track</string>
|
||||||
<!-- OpenTracks -->
|
<!-- OpenTracks -->
|
||||||
<string name="recording_service">Recording service</string>
|
<string name="recording_service">Recording service</string>
|
||||||
|
<string name="delete_service">Delete service</string>
|
||||||
<!-- Permission -->
|
<!-- Permission -->
|
||||||
<string name="permission_gps_failed">OpenTracks requires permission to use GPS.</string>
|
<string name="permission_gps_failed">OpenTracks requires permission to use GPS.</string>
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
@@ -475,6 +476,7 @@ limitations under the License.
|
|||||||
<string name="track_delete_one_confirm_title">Delete track?</string>
|
<string name="track_delete_one_confirm_title">Delete track?</string>
|
||||||
<string name="track_delete_multiple_confirm_message">The selected tracks and their markers will be permanently deleted from the device.</string>
|
<string name="track_delete_multiple_confirm_message">The selected tracks and their markers will be permanently deleted from the device.</string>
|
||||||
<string name="track_delete_progress_message">Deleting…</string>
|
<string name="track_delete_progress_message">Deleting…</string>
|
||||||
|
<string name="track_delete_progress">%1$d/%2$d</string>
|
||||||
<!-- Track Detail -->
|
<!-- Track Detail -->
|
||||||
<string name="track_detail_chart_tab">Chart</string>
|
<string name="track_detail_chart_tab">Chart</string>
|
||||||
<string name="track_detail_stats_tab">Stats</string>
|
<string name="track_detail_stats_tab">Stats</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user