From 1d838344dc8c587ea044d8732f2ddf13c95c1813 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Wed, 15 Apr 2020 22:37:31 +0200 Subject: [PATCH] TrackRecordingService: test inserting locations. --- .../services/TrackRecordingServiceTest.java | 23 +- .../TrackRecordingServiceTestLocation.java | 329 ++++++++++++++++++ .../opentracks/content/data/TrackPoint.java | 6 + .../content/sensor/SensorDataSet.java | 20 +- .../services/TrackRecordingService.java | 29 +- .../services/TrackRecordingServiceBinder.java | 13 + .../TrackRecordingServiceInterface.java | 10 + 7 files changed, 412 insertions(+), 18 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java index bbbe58e7c..7ebc2bb5c 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java @@ -206,7 +206,7 @@ public class TrackRecordingServiceTest { Assert.assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size()); - //when + // when service.resumeTrack(trackId); insertLocation(service); @@ -331,19 +331,28 @@ public class TrackRecordingServiceTest { addTrack(dummyTrack, isRecording); } + static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException { + insertLocation(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis()); + } + + static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException { + insertLocation(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis()); + } + /** * Inserts a location and waits for 200ms. */ - private void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException { + static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException { Location location = new Location("gps"); - location.setLongitude(35.0f); - location.setLatitude(45.0f); - location.setAccuracy(5); - location.setSpeed(10); - location.setTime(System.currentTimeMillis()); + location.setLongitude(longitude); + location.setLatitude(latitude); + location.setAccuracy(accuracy); + location.setSpeed(speed); + location.setTime(time); location.setBearing(3.0f); trackRecordingService.insertLocation(location); + //TODO Needed? Thread.sleep(200); } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java new file mode 100644 index 000000000..eaeda2d6c --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java @@ -0,0 +1,329 @@ +package de.dennisguse.opentracks.services; + +import android.content.ContentProvider; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Looper; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.MediumTest; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.rule.ServiceTestRule; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.content.data.TrackPointsColumns; +import de.dennisguse.opentracks.content.provider.ContentProviderUtils; +import de.dennisguse.opentracks.content.provider.CustomContentProvider; +import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; +import de.dennisguse.opentracks.util.PreferencesUtils; + +/** + * Tests insert location. + *

+ * //TODO ATTENTION: This tests deletes all stored tracks in the database. + * So, if it is executed on a real device, data might be lost. + */ +@RunWith(AndroidJUnit4.class) +public class TrackRecordingServiceTestLocation { + + @Rule + public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); + + @Rule + public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); + + private final Context context = ApplicationProvider.getApplicationContext(); + private ContentProviderUtils contentProviderUtils; + + private TrackRecordingServiceInterface service; + + @BeforeClass + public static void preSetUp() { + // Prepare looper for Android's message queue + if (Looper.myLooper() == null) Looper.prepare(); + } + + @Before + public void setUp() throws TimeoutException { + // Set up the mock content resolver + ContentProvider customContentProvider = new CustomContentProvider() { + }; + customContentProvider.attachInfo(context, null); + + contentProviderUtils = new ContentProviderUtils(context); + + // Let's use default values. + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + sharedPreferences.edit().clear().commit(); + + service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context))); + //Disable executorService to not insert locations from GPS via LocationManager + service.enableLocationExecutor(false); + } + + @After + public void tearDown() throws TimeoutException { + // Reset service (if some previous test failed) + service.enableLocationExecutor(true); + if (service.isRecording() || service.isPaused()) { + service.endCurrentTrack(); + } + + // Ensure that the database is empty after every test + contentProviderUtils.deleteAllTracks(context); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_movingAccurate() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0001, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0002, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0003, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0004, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0005, 35.0, 5, 15); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(6, trackPoints.size()); + Assert.assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_movingInaccurate() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, Long.MAX_VALUE, 15); + TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, Long.MAX_VALUE, 15); + TrackRecordingServiceTest.insertLocation(service, 45.3, 35.0, Long.MAX_VALUE, 15); + TrackRecordingServiceTest.insertLocation(service, 99.0, 35.0, Long.MAX_VALUE, 15); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(1, trackPoints.size()); + Assert.assertEquals(45.0, trackPoints.get(0).getLatitude(), 0.01); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.000001, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.000002, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.000003, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.000004, 35.0, 5, 15); + TrackRecordingServiceTest.insertLocation(service, 45.000005, 35.0, 5, 15); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(2, trackPoints.size()); + Assert.assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01); + } + +// @MediumTest +// @Test +// public void testOnLocationChangedAsync_repeatedTime() throws Exception { +// // when +// TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15, 5); +// TrackRecordingServiceTest.insertLocation(service, 55.0, 35.0, 5, 15, 5); +// TrackRecordingServiceTest.insertLocation(service, 65.0, 35.0, 5, 15, 5); +// +// service.endCurrentTrack(); +// +// // then +// Assert.assertFalse(service.isRecording()); +// +// List trackPoints = contentProviderUtils.getTrackPoints(trackId); +// Assert.assertEquals(1, trackPoints.size()); +// Assert.assertEquals(45.0, trackPoints.get(0).getLatitude(), 0.01); +// } + + @MediumTest + @Test + public void testOnLocationChangedAsync_idle() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 6, 0); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(3, trackPoints.size()); + Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01); + Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01); + Assert.assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_idle_withMovement() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 15); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 6, 15); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(4, trackPoints.size()); + Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01); + Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01); + Assert.assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted. + Assert.assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01); + } + + + @MediumTest + @Test + public void testOnLocationChangedAsync_idle_withSensorData() throws Exception { + // given + long trackId = service.startNewTrack(); + + service.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) { + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isSensorDataSetValid() { + return true; + } + + @Override + public SensorDataSet getSensorDataSet() { + return new SensorDataSet(1, 2); + } + }); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 0, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(6, trackPoints.size()); + Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01); + Assert.assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01); + Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01); + Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01); + Assert.assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01); + Assert.assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_segment() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0); + TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, 2, 0); + TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, 3, 0); + TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, 4, 0); + TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, 5, 0); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(7, trackPoints.size()); + Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01); + Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01); + Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01); + Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01); + Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01); + Assert.assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01); + Assert.assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01); + } + + @MediumTest + @Test + public void testOnLocationChangedAsync_firstTrackPointInvalid() throws Exception { + // given + long trackId = service.startNewTrack(); + + // when + service.insertLocation(TrackPoint.createPause().getLocation()); + TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 0, 0); + service.insertLocation(TrackPoint.createPause().getLocation()); + + service.endCurrentTrack(); + + // then + Assert.assertFalse(service.isRecording()); + + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertEquals(1, trackPoints.size()); + Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index 9569bffca..a3f230bb8 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -181,4 +181,10 @@ public class TrackPoint { public float bearingTo(@NonNull Location dest) { return location.bearingTo(dest); } + + @NonNull + @Override + public String toString() { + return "time=" + getTime() + ": lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy(); + } } diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java index 971816de2..60b1677ca 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -1,5 +1,7 @@ package de.dennisguse.opentracks.content.sensor; +import androidx.annotation.NonNull; + public final class SensorDataSet { public static final float DATA_UNAVAILABLE = Float.NaN; @@ -11,14 +13,14 @@ public final class SensorDataSet { private float cadence; private float power; private float batteryLevel; - private long creationTimestamp; + private long time; - public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long creationTimestamp) { + public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long time) { this.heartRate = heartRate; this.cadence = cadence; this.power = power; this.batteryLevel = batteryLevel; - this.creationTimestamp = creationTimestamp; + this.time = time; } public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) { @@ -63,8 +65,8 @@ public final class SensorDataSet { return power; } - public long getCreationTime() { - return creationTimestamp; + public long getTime() { + return time; } /** @@ -73,7 +75,7 @@ public final class SensorDataSet { * @param maxAge the maximal age in milliseconds. */ public boolean isRecent(long maxAge) { - return creationTimestamp + maxAge > System.currentTimeMillis(); + return time + maxAge > System.currentTimeMillis(); } public boolean hasBatteryLevel() { @@ -91,4 +93,10 @@ public final class SensorDataSet { public String getSensorAddress() { return sensorAddress; } + + @NonNull + @Override + public String toString() { + return "time=" + getTime() + " sensor=" + getSensorAddress() + " heart=" + getHeartRate(); + } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 13ac70f86..8ce1ec2de 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -34,6 +34,7 @@ import android.os.PowerManager.WakeLock; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.core.app.TaskStackBuilder; import java.util.concurrent.ExecutorService; @@ -75,7 +76,7 @@ public class TrackRecordingService extends Service { // The following variables are set in onCreate: @Deprecated //TODO Should not be necessary - private ExecutorService executorService; // Enforces order of location changes. + private ExecutorService locationExecutorService; // Enforces order of location changes. private ContentProviderUtils contentProviderUtils; private LocationManager locationManager; private PeriodicTaskExecutor voiceExecutor; @@ -146,10 +147,10 @@ public class TrackRecordingService extends Service { @Override public void onLocationChanged(final Location location) { - if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) { + if (locationExecutorService == null || locationExecutorService.isShutdown() || locationExecutorService.isTerminated()) { return; } - executorService.submit(new Runnable() { + locationExecutorService.submit(new Runnable() { @Override public void run() { onLocationChangedAsync(location); @@ -176,7 +177,7 @@ public class TrackRecordingService extends Service { @Override public void onCreate() { super.onCreate(); - executorService = Executors.newSingleThreadExecutor(); + locationExecutorService = Executors.newSingleThreadExecutor(); contentProviderUtils = new ContentProviderUtils(this); locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); @@ -232,7 +233,7 @@ public class TrackRecordingService extends Service { wakeLock = SystemUtils.releaseWakeLock(wakeLock); // Shutdown the executorService last to avoid sending events to a dead executor. - executorService.shutdown(); + locationExecutorService.shutdown(); super.onDestroy(); } @@ -763,4 +764,22 @@ public class TrackRecordingService extends Service { notificationManager.cancelNotification(); } } + + /** + * Disables processing of location updates from {@link android.location.LocationManager}. + */ + @VisibleForTesting + public void enableLocationExecutor(boolean enable) { + if (enable) { + locationExecutorService = Executors.newSingleThreadExecutor(); + } else { + locationExecutorService.shutdownNow(); + locationExecutorService = null; + } + } + + @VisibleForTesting + public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) { + this.remoteSensorManager = remoteSensorManager; + } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java index 3836d05a0..f5ec8d004 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java @@ -5,6 +5,7 @@ import android.location.Location; import androidx.annotation.VisibleForTesting; import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; /** * TODO: There is a bug in Android that leaks Binder instances. This bug is @@ -92,6 +93,18 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco return trackRecordingService.getSensorDataSet(); } + @VisibleForTesting + @Override + public void enableLocationExecutor(boolean enable) { + trackRecordingService.enableLocationExecutor(enable); + } + + @VisibleForTesting + @Override + public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) { + trackRecordingService.setRemoteSensorManager(remoteSensorManager); + } + /** * Detaches from the track recording service. Clears the reference to the * outer class to minimize the leak. diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java index bc01c1dff..531206a4a 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java @@ -20,6 +20,7 @@ import android.location.Location; import androidx.annotation.VisibleForTesting; import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; /** * App's service. @@ -112,4 +113,13 @@ public interface TrackRecordingServiceInterface { * @return SensorDataSet object. */ SensorDataSet getSensorData(); + + /** + * Disables processing of location updates from {@link android.location.LocationManager}. + */ + @VisibleForTesting + void enableLocationExecutor(boolean enable); + + @VisibleForTesting + void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager); }