Merge pull request #187 from OpenTracksApp/dev

Refactored TrackRecordingService.
This commit is contained in:
Dennis Guse
2020-04-15 23:11:38 +02:00
committed by GitHub
22 changed files with 998 additions and 488 deletions
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks;
import android.location.Location; import android.location.Location;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -40,21 +39,6 @@ public class TrackStubUtils {
// Used to change the value of latitude, longitude, and altitude. // Used to change the value of latitude, longitude, and altitude.
private static final double DIFFERENCE = 0.01; private static final double DIFFERENCE = 0.01;
/**
* Gets a a {@link Track} stub with specified number of locations.
*
* @param numberOfLocations the number of locations for the track
* @return a track stub.
*/
public static Track createTrack(int numberOfLocations) {
Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) {
track.addTrackPoint(createDefaultTrackPoint(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
}
/** /**
* Create a MyTracks location with default values. * Create a MyTracks location with default values.
* *
@@ -1,6 +1,9 @@
package de.dennisguse.opentracks.content.data; package de.dennisguse.opentracks.content.data;
import android.location.Location; import android.location.Location;
import android.util.Pair;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
public class TestDataUtil { public class TestDataUtil {
@@ -8,21 +11,40 @@ public class TestDataUtil {
public static final double INITIAL_LONGITUDE = -57.0; public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5; public static final double ALTITUDE_INTERVAL = 2.5;
/**
* Create a track without any trackPoints.
*/
public static Track createTrack(long trackId) {
Track track = new Track();
track.setId(trackId);
track.setName("Test: " + trackId);
return track;
}
/** /**
* Simulates a track which is used for testing. * Simulates a track which is used for testing.
* *
* @param id the id of the track * @param trackId the trackId of the track
* @param numPoints the location number in the track * @param numPoints the trackPoints number in the track
* @return the simulated track
*/ */
public static Track getTrack(long id, int numPoints) { public static Pair<Track, TrackPoint[]> createTrack(long trackId, int numPoints) {
Track track = new Track(); Track track = createTrack(trackId);
track.setId(id);
track.setName("Test: " + id); TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; i++) { for (int i = 0; i < numPoints; i++) {
track.addTrackPoint(createTrackPoint(i)); trackPoints[i] = (createTrackPoint(i));
} }
return track;
return new Pair<>(track, trackPoints);
}
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, long trackId, int numPoints) {
Pair<Track, TrackPoint[]> pair = createTrack(trackId, numPoints);
insertTrackWithLocations(contentProviderUtils, pair.first, pair.second);
return pair.first;
} }
/** /**
@@ -40,4 +62,15 @@ public class TestDataUtil {
location.setTime(i + 1); location.setTime(i + 1);
return new TrackPoint(location); return new TrackPoint(location);
} }
/**
* Inserts a track with locations into the database.
*
* @param track track to be inserted
* @param trackPoints trackPoints to be inserted
*/
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, TrackPoint[] trackPoints) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, track.getId());
}
} }
@@ -21,6 +21,7 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@@ -32,6 +33,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -141,7 +143,7 @@ public class CustomContentProviderUtilsTest {
loc.setAltitude(i * 2.5); loc.setAltitude(i * 2.5);
trackPoints[i] = new TrackPoint(loc); trackPoints[i] = new TrackPoint(loc);
} }
contentProviderUtils.bulkInsertTrackPoint(trackPoints, numPoints, id); contentProviderUtils.bulkInsertTrackPoint(trackPoints, id);
// Load all inserted trackPoints. // Load all inserted trackPoints.
long lastPointId = -1; long lastPointId = -1;
@@ -194,10 +196,11 @@ public class CustomContentProviderUtilsTest {
public void testDeleteAllTracks() { public void testDeleteAllTracks() {
// Insert track, points and waypoint at first. // Insert track, points and waypoint at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
insertTrackWithLocations(track);
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertWaypoint(waypoint); contentProviderUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID); Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(1, tracksCursor.getCount()); Assert.assertEquals(1, tracksCursor.getCount());
@@ -222,14 +225,14 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testDeleteTrack() { public void testDeleteTrack() {
// Insert three tracks, points of two tracks and way point of one track. // Insert three tracks, points of two tracks and way point of one track.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 0);
contentProviderUtils.insertTrack(track); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 1, 10);
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 1, 10)); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 2, 10);
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 2, 10));
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId + 1));
waypoint.setTrackId(trackId); waypoint.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint); contentProviderUtils.insertWaypoint(waypoint);
@@ -256,10 +259,15 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testGetAllTracks() { public void testGetAllTracks() {
// given
int initialTrackNumber = contentProviderUtils.getAllTracks().size(); int initialTrackNumber = contentProviderUtils.getAllTracks().size();
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0)); contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
// when
List<Track> allTracks = contentProviderUtils.getAllTracks(); List<Track> allTracks = contentProviderUtils.getAllTracks();
// then
Assert.assertEquals(initialTrackNumber + 1, allTracks.size()); Assert.assertEquals(initialTrackNumber + 1, allTracks.size());
Assert.assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId()); Assert.assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId());
} }
@@ -270,7 +278,7 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testGetLastTrack() { public void testGetLastTrack() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0)); contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
Assert.assertEquals(trackId, contentProviderUtils.getLastTrack().getId()); Assert.assertEquals(trackId, contentProviderUtils.getLastTrack().getId());
} }
@@ -280,7 +288,7 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testGetTrack() { public void testGetTrack() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0)); contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
Assert.assertNotNull(contentProviderUtils.getTrack(trackId)); Assert.assertNotNull(contentProviderUtils.getTrack(trackId));
} }
@@ -289,11 +297,14 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testUpdateTrack() { public void testUpdateTrack() {
// given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 0); Track track = TestDataUtil.createTrack(trackId);
String nameOld = "name1"; String nameOld = "name1";
String nameNew = "name2"; String nameNew = "name2";
track.setName(nameOld); track.setName(nameOld);
// when / then
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track);
Assert.assertEquals(nameOld, contentProviderUtils.getTrack(trackId).getName()); Assert.assertEquals(nameOld, contentProviderUtils.getTrack(trackId).getName());
track.setName(nameNew); track.setName(nameNew);
@@ -307,7 +318,8 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testCreateContentValues_waypoint() { public void testCreateContentValues_waypoint() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
// Bottom // Bottom
long startTime = 1000L; long startTime = 1000L;
// AverageSpeed // AverageSpeed
@@ -322,17 +334,10 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxElevation(1250.0); statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0); statistics.setMinElevation(1200.0);
track.setTrackStatistics(statistics); track.first.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
// Insert at first. Waypoint waypoint = new Waypoint(track.second[0]);
Location location = new Location("test");
location.setLatitude(22);
location.setLongitude(22);
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
Waypoint waypoint = new Waypoint(location);
waypoint.setDescription(TEST_DESC); waypoint.setDescription(TEST_DESC);
contentProviderUtils.insertWaypoint(waypoint); contentProviderUtils.insertWaypoint(waypoint);
@@ -342,7 +347,7 @@ public class CustomContentProviderUtilsTest {
waypoint.setId(waypointId); waypoint.setId(waypointId);
ContentValues contentValues = contentProviderUtils.createContentValues(waypoint); ContentValues contentValues = contentProviderUtils.createContentValues(waypoint);
Assert.assertEquals(waypointId, contentValues.get(WaypointsColumns._ID)); Assert.assertEquals(waypointId, contentValues.get(WaypointsColumns._ID));
Assert.assertEquals(22 * 1000000, contentValues.get(WaypointsColumns.LONGITUDE)); Assert.assertEquals((int) (TestDataUtil.INITIAL_LONGITUDE * 1000000), contentValues.get(WaypointsColumns.LONGITUDE));
Assert.assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION)); Assert.assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION));
} }
@@ -388,11 +393,10 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testDeleteWaypoint_onlyOneWayPoint() { public void testDeleteWaypoint_onlyOneWayPoint() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrack(track);
// Insert at first. // Insert at first.
Waypoint waypoint1 = new Waypoint(); Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(TEST_DESC); waypoint1.setDescription(TEST_DESC);
waypoint1.setTrackId(trackId); waypoint1.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint1); contentProviderUtils.insertWaypoint(waypoint1);
@@ -404,37 +408,36 @@ public class CustomContentProviderUtilsTest {
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtils#deleteWaypoint(long)} when there is more than one waypoint in the track.
* {@link ContentProviderUtils#deleteWaypoint(long)}
* when there is more than one waypoint in the track.
*/ */
@Test @Test
public void testDeleteWaypoint_hasNextWayPoint() { public void testDeleteWaypoint_hasNextWayPoint() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(1000L);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
statistics.setTotalElevationGain(50.0);
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Track track = TestDataUtil.createTrackAndInsert(trackId, 10);
//
// TrackStatistics statistics = new TrackStatistics();
// statistics.setStartTime_ms(1000L);
// statistics.setStopTime_ms(2500L);
// statistics.setTotalTime(1500L);
// statistics.setMovingTime(700L);
// statistics.setTotalDistance(750.0);
// statistics.setTotalElevationGain(50.0);
// statistics.setMaxSpeed(60.0);
// statistics.setMaxElevation(1250.0);
// statistics.setMinElevation(1200.0);
//
// track.setTrackStatistics(statistics);
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
// Insert at first. // Insert at first.
Waypoint waypoint1 = new Waypoint(); Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(MOCK_DESC); waypoint1.setDescription(MOCK_DESC);
waypoint1.setTrackId(trackId); waypoint1.setTrackId(trackId);
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1)); long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint(); Waypoint waypoint2 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint2.setDescription(MOCK_DESC); waypoint2.setDescription(MOCK_DESC);
waypoint2.setTrackId(trackId); waypoint2.setTrackId(trackId);
long waypoint2Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint2)); long waypoint2Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint2));
@@ -453,16 +456,15 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testGetNextWaypointNumber() { public void testGetNextWaypointNumber() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint1 = new Waypoint(); Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setTrackId(trackId); waypoint1.setTrackId(trackId);
Waypoint waypoint2 = new Waypoint(); Waypoint waypoint2 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint2.setTrackId(trackId); waypoint2.setTrackId(trackId);
Waypoint waypoint3 = new Waypoint(); Waypoint waypoint3 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint3.setTrackId(trackId); waypoint3.setTrackId(trackId);
Waypoint waypoint4 = new Waypoint(); Waypoint waypoint4 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint4.setTrackId(trackId); waypoint4.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint1); contentProviderUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint2); contentProviderUtils.insertWaypoint(waypoint2);
@@ -479,10 +481,9 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testInsertAndGetWaypoint() { public void testInsertAndGetWaypoint() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC); waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId); waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint)); long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
@@ -496,10 +497,10 @@ public class CustomContentProviderUtilsTest {
@Test @Test
public void testUpdateWaypoint() { public void testUpdateWaypoint() {
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrack(track);
// Insert at first. // Insert at first.
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC); waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId); waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint)); long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
@@ -517,14 +518,15 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testBulkInsertTrackPoint() { public void testBulkInsertTrackPoint() {
// Insert track, point at first. // given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
insertTrackWithLocations(track); TestDataUtil.insertTrackWithLocations(contentProviderUtils, track.first, track.second);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), -1, trackId); // when / then
contentProviderUtils.bulkInsertTrackPoint(track.second, trackId);
Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount()); Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), 8, trackId); contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(track.second, 0, 8), trackId);
Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount()); Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
} }
@@ -535,48 +537,32 @@ public class CustomContentProviderUtilsTest {
public void testCreateTrackPoint() { public void testCreateTrackPoint() {
// Set index. // Set index.
int index = 1; int index = 1;
// Id
when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(index++); when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(index++);
// Longitude
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(index++); when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(index++);
// Latitude
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(index++); when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(index++);
// Time when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME)).thenReturn(index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME))
.thenReturn(index++);
// Speed
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(index++); when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(index++);
// Sensor
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(index++); when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(index++);
// Set return value of isNull(). // Set return value of isNull().
index = 2; index = 2;
// Longitude
when(cursorMock.isNull(index++)).thenReturn(false); when(cursorMock.isNull(index++)).thenReturn(false);
// Latitude
when(cursorMock.isNull(index++)).thenReturn(false); when(cursorMock.isNull(index++)).thenReturn(false);
// Time
when(cursorMock.isNull(index++)).thenReturn(false); when(cursorMock.isNull(index++)).thenReturn(false);
// Speed
when(cursorMock.isNull(index++)).thenReturn(false); when(cursorMock.isNull(index++)).thenReturn(false);
// Sensor
when(cursorMock.isNull(index++)).thenReturn(false); when(cursorMock.isNull(index++)).thenReturn(false);
// Set return value of isNull(). // Set return value of getInt().
index = 2; index = 2;
// Longitude
int longitude = 11; int longitude = 11;
when(cursorMock.getInt(index++)).thenReturn(longitude * 1000000); when(cursorMock.getInt(index++)).thenReturn(longitude * 1000000);
// Latitude.
int latitude = 22; int latitude = 22;
when(cursorMock.getInt(index++)).thenReturn(latitude * 1000000); when(cursorMock.getInt(index++)).thenReturn(latitude * 1000000);
// Time
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
when(cursorMock.getLong(index++)).thenReturn(time); when(cursorMock.getLong(index++)).thenReturn(time);
// Speed
float speed = 2.2f; float speed = 2.2f;
when(cursorMock.getFloat(index++)).thenReturn(speed); when(cursorMock.getFloat(index++)).thenReturn(speed);
// Sensor
byte[] sensor = "Sensor state".getBytes(); byte[] sensor = "Sensor state".getBytes();
when(cursorMock.getBlob(index++)).thenReturn(sensor); when(cursorMock.getBlob(index++)).thenReturn(sensor);
@@ -595,8 +581,7 @@ public class CustomContentProviderUtilsTest {
public void testInsertTrackPoint() { public void testInsertTrackPoint() {
// Insert track, point at first. // Insert track, point at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
insertTrackWithLocations(track);
contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId); contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId);
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount()); Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
@@ -609,8 +594,7 @@ public class CustomContentProviderUtilsTest {
public void testGetLastValidTrackPoint() { public void testGetLastValidTrackPoint() {
// Insert track, points at first. // Insert track, points at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
insertTrackWithLocations(track);
TrackPoint lastTrackPoint = contentProviderUtils.getLastValidTrackPoint(trackId); TrackPoint lastTrackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastTrackPoint.getLocation()); checkLocation(9, lastTrackPoint.getLocation());
@@ -621,17 +605,20 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testGetTrackPointCursor_desc() { public void testGetTrackPointCursor_desc() {
// Insert track, points at first. // given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()]; long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId())); trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
} }
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true); Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
// then
Assert.assertEquals(2, cursor.getCount()); Assert.assertEquals(2, cursor.getCount());
} }
@@ -640,17 +627,20 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testGetTrackPointCursor_asc() { public void testGetTrackPointCursor_asc() {
// Insert track, points at first. // given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()]; long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId())); trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
} }
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false); Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
// then
Assert.assertEquals(2, cursor.getCount()); Assert.assertEquals(2, cursor.getCount());
} }
@@ -659,19 +649,21 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_desc() { public void testGetTrackPointLocationIterator_desc() {
// Insert track, points at first. // given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()]; long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId())); trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
} }
long startTrackPointId = trackpointIds[9]; long startTrackPointId = trackpointIds[9];
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true); TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true);
// then
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(trackPointIterator.hasNext()); Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next(); TrackPoint trackPoint = trackPointIterator.next();
@@ -686,33 +678,36 @@ public class CustomContentProviderUtilsTest {
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_asc() { public void testGetTrackPointLocationIterator_asc() {
// Insert track, point at first. // given
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()]; long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId())); trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
} }
long startTrackPointId = trackpointIds[0]; long startTrackPointId = trackpointIds[0];
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false); // when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
// then
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = locationIterator.next(); TrackPoint trackPoint = trackPointIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId()); Assert.assertEquals(startTrackPointId + i, trackPointIterator.getTrackPointId());
checkLocation(i, trackPoint.getLocation()); checkLocation(i, trackPoint.getLocation());
} }
Assert.assertFalse(locationIterator.hasNext()); Assert.assertFalse(trackPointIterator.hasNext());
} }
/** /**
* Checks the value of a location. * Checks the value of a location.
* *
* @param i the index of this location which created in the method {@link TestDataUtil#getTrack(long, int)} * @param i the index of this location which created in the method {@link TestDataUtil#createTrack(long, int)}
* @param location the location to be checked * @param location the location to be checked
*/ */
private void checkLocation(int i, Location location) { private void checkLocation(int i, Location location) {
@@ -722,15 +717,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(i * TestDataUtil.ALTITUDE_INTERVAL, location.getAltitude(), 0.01); Assert.assertEquals(i * TestDataUtil.ALTITUDE_INTERVAL, location.getAltitude(), 0.01);
} }
/**
* Inserts a track with locations into the database.
*
* @param track track to be inserted
*/
private void insertTrackWithLocations(Track track) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
}
@Test @Test
public void testFormatIdListForUri() { public void testFormatIdListForUri() {
@@ -101,7 +101,7 @@ public abstract class AbstractTestFileTrackImporter {
* @param trackPointId the track point id * @param trackPointId the track point id
*/ */
protected void expectFirstTrackPoint(TrackPoint trackPoint, long trackId, long trackPointId) { protected void expectFirstTrackPoint(TrackPoint trackPoint, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(1), eq(trackId))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(trackId))).thenReturn(1);
} }
/** /**
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
@@ -53,15 +54,15 @@ public class ExportImportTest {
@Before @Before
public void setUp() { public void setUp() {
Track track = TestDataUtil.getTrack(trackId, 10); Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
track.setIcon(TRACK_ICON); track.first.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY); track.first.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION); track.first.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track); contentProviderUtils.insertTrack(track.first);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId()); contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint(track.getTrackPoints().get(i).getLocation()); Waypoint waypoint = new Waypoint(track.second[i].getLocation());
waypoint.setName("the waypoint " + i); waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i); waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i); waypoint.setCategory("the waypoint category" + i);
@@ -114,7 +115,7 @@ public class ExportImportTest {
// 1. track // 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId); Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack); assertNotNull(importedTrack);
assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints()); //TODO assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
assertEquals(track.getCategory(), importedTrack.getCategory()); assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription()); assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName()); assertEquals(track.getName(), importedTrack.getName());
@@ -184,7 +185,7 @@ public class ExportImportTest {
// 1. track // 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId); Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported); assertNotNull(trackImported);
assertEquals(track.getTrackPoints(), trackImported.getTrackPoints()); //TODO assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
assertEquals(track.getCategory(), trackImported.getCategory()); assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription()); assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName()); assertEquals(track.getName(), trackImported.getName());
@@ -30,7 +30,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
@@ -91,7 +90,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -120,7 +119,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -152,7 +151,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -206,7 +205,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are invoked or not. // For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyInt(), anyLong())).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyLong())).thenReturn(1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0); contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -75,7 +75,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -106,7 +106,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -24,6 +24,7 @@ import android.os.IBinder;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.MediumTest; import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.GrantPermissionRule;
@@ -43,6 +44,8 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider; import de.dennisguse.opentracks.content.provider.CustomContentProvider;
@@ -86,7 +89,7 @@ public class TrackRecordingServiceTest {
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply(); sharedPreferences.edit().clear().commit();
// Ensure that the database is empty before every test // Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context); contentProviderUtils.deleteAllTracks(context);
@@ -121,13 +124,16 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testRecording_noTracks() throws Exception { public void testRecording_noTracks() throws Exception {
// given
List<Track> tracks = contentProviderUtils.getAllTracks(); List<Track> tracks = contentProviderUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty()); Assert.assertTrue(tracks.isEmpty());
// when
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent); mServiceRule.startService(startIntent);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent)); TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
// then
// Test if we start in no-recording mode by default. // Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(-1L, service.getRecordingTrackId());
@@ -136,19 +142,92 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testRecording_oldTracks() throws Exception { public void testRecording_oldTracks() throws Exception {
// given
createDummyTrack(trackId, -1L, false); createDummyTrack(trackId, -1L, false);
// when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context))); TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@Test @Test
public void testRecording_orphanedRecordingTrack() throws Exception { public void testRecording_serviceRestart_whileRecording() throws Exception {
Intent startIntent = createStartIntent(context); // given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent)); createDummyTrack(trackId, -1L, true);
//when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testRecording_pauseAndResume() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
// when
service.pauseCurrentTrack();
// then
Assert.assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
//when
service.resumeTrack(trackId);
insertLocation(service);
// then
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(5, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
service.endCurrentTrack();
Assert.assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.resumeTrack(trackId);
insertLocation(service);
// then
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(4, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
}
@FlakyTest(detail = "Sometimes fails on CI.")
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// when
// Just set recording track to a bogus value. // Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track. // Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L); PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
@@ -161,33 +240,86 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testStartNewTrack_alreadyRecording() throws Exception { public void testStartNewTrack_alreadyRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context))); TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack(); service.startNewTrack();
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId(); long trackId = service.getRecordingTrackId();
// when
long newTrackId = service.startNewTrack(); long newTrackId = service.startNewTrack();
// then
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context)); Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
service.endCurrentTrack();
} }
@MediumTest @MediumTest
@Test @Test
public void testEndCurrentTrack_noRecording() throws Exception { public void testEndCurrentTrack_noRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context))); TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
// when
// Ending the current track when there is no recording should not result in any error. // Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack(); service.endCurrentTrack();
// then
Assert.assertFalse(PreferencesUtils.isRecording(context)); Assert.assertFalse(PreferencesUtils.isRecording(context));
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// when
long waypointId = service.insertWaypoint(null, null, null, null);
// then
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long trackId = service.getRecordingTrackId();
// when
long waypointId = service.insertWaypoint(null, null, null, null);
// then
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
private void addTrack(Track track, boolean isRecording) {
Assert.assertTrue(track.getId() >= 0);
contentProviderUtils.insertTrack(track);
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
}
// NOTE: Do not use to create a track that is currently recording. // NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(long id, long stopTime, boolean isRecording) { private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track(); Track dummyTrack = new Track();
@@ -199,58 +331,28 @@ public class TrackRecordingServiceTest {
addTrack(dummyTrack, isRecording); addTrack(dummyTrack, isRecording);
} }
private void addTrack(Track track, boolean isRecording) { static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
Assert.assertTrue(track.getId() >= 0); insertLocation(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
contentProviderUtils.insertTrack(track); }
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT); static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording); insertLocation(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
} }
/** /**
* Inserts a location and waits for 200ms. * 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 location = new Location("gps");
location.setLongitude(35.0f); location.setLongitude(longitude);
location.setLatitude(45.0f); location.setLatitude(latitude);
location.setAccuracy(5); location.setAccuracy(accuracy);
location.setSpeed(10); location.setSpeed(speed);
location.setTime(System.currentTimeMillis()); location.setTime(time);
location.setBearing(3.0f); location.setBearing(3.0f);
trackRecordingService.insertLocation(location); trackRecordingService.insertLocation(location);
//TODO Needed?
Thread.sleep(200); Thread.sleep(200);
} }
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
} }
@@ -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.
* <p>
* //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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> 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<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(1, trackPoints.size());
Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
}
}
@@ -16,11 +16,6 @@
package de.dennisguse.opentracks.content.data; package de.dennisguse.opentracks.content.data;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
/** /**
@@ -40,9 +35,6 @@ public class Track {
private TrackStatistics trackStatistics = new TrackStatistics(); private TrackStatistics trackStatistics = new TrackStatistics();
// Location points (which may not have been loaded)
private List<TrackPoint> trackPoints = new ArrayList<>();
public Track() { public Track() {
} }
@@ -93,17 +85,4 @@ public class Track {
public void setTrackStatistics(TrackStatistics trackStatistics) { public void setTrackStatistics(TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics; this.trackStatistics = trackStatistics;
} }
@VisibleForTesting
public void addTrackPoint(TrackPoint location) {
trackPoints.add(location);
}
public List<TrackPoint> getTrackPoints() {
return trackPoints;
}
public void setTrackPoints(ArrayList<TrackPoint> trackPoints) {
this.trackPoints = trackPoints;
}
} }
@@ -181,4 +181,10 @@ public class TrackPoint {
public float bearingTo(@NonNull Location dest) { public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest); return location.bearingTo(dest);
} }
@NonNull
@Override
public String toString() {
return "time=" + getTime() + ": lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy();
}
} }
@@ -38,14 +38,15 @@ public final class Waypoint {
private long trackId = -1L; private long trackId = -1L;
private double length = 0.0; private double length = 0.0;
private long duration = 0; private long duration = 0;
private Location location = null; private Location location;
private String photoUrl = ""; private String photoUrl = "";
@VisibleForTesting @VisibleForTesting
public Waypoint() { public Waypoint(@NonNull TrackPoint trackPoint) {
this.location = trackPoint.getLocation();
} }
public Waypoint(Location location) { public Waypoint(@NonNull Location location) {
this.location = location; this.location = location;
} }
@@ -55,19 +55,13 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName(); private static final String TAG = ContentProviderUtils.class.getSimpleName();
private static final int MAX_LATITUDE = 90000000; private static final int MAX_LATITUDE = 90000000;
/** // The authority (the first part of the URI) for the app's content provider.
* The authority (the first part of the URI) for the app's content provider.
*/
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content"; static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
/** // The base URI for the app's content provider.
* The base URI for the app's content provider.
*/
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE; public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/** // Maximum number of waypoints that will be loaded at one time.
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000; public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
private static final String ID_SEPARATOR = ","; private static final String ID_SEPARATOR = ",";
@@ -87,7 +81,7 @@ public class ContentProviderUtils {
} }
/** /**
* Clears a track: removes waypoints and trackpoints. * Clears a track: removes waypoints and trackPoints.
* Only keeps the track id. * Only keeps the track id.
* *
* @param trackId the track id * @param trackId the track id
@@ -168,7 +162,7 @@ public class ContentProviderUtils {
} }
/** /**
* Deletes all tracks (including waypoints and track points). * Deletes all tracks (including waypoints and trackPoints).
*/ */
public void deleteAllTracks(Context context) { public void deleteAllTracks(Context context) {
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null); contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
@@ -188,13 +182,12 @@ public class ContentProviderUtils {
public void deleteTrack(Context context, long trackId) { public void deleteTrack(Context context, long trackId) {
deleteTrackPointsAndWaypoints(context, trackId); deleteTrackPointsAndWaypoints(context, trackId);
// Delete track last since it triggers a database vaccum call // Delete track last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId)});
new String[]{Long.toString(trackId)});
} }
/** /**
* Deletes track points and waypoints of a track. * Deletes trackPoints and waypoints of a track.
* *
* @param trackId the track id * @param trackId the track id
*/ */
@@ -203,8 +196,7 @@ public class ContentProviderUtils {
String[] selectionArgs = new String[]{Long.toString(trackId)}; String[] selectionArgs = new String[]{Long.toString(trackId)};
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, where, selectionArgs); contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, where, selectionArgs);
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?", contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?", new String[]{Long.toString(trackId)});
new String[]{Long.toString(trackId)});
deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId)); deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
} }
@@ -224,8 +216,8 @@ public class ContentProviderUtils {
/** /**
* Gets all the tracks. * Gets all the tracks.
* If no track exists, an empty list is returned. *
* NOTE: the returned tracks do not have any track points attached. * @return the tracks do not have any trackPoints attached.
*/ */
@VisibleForTesting @VisibleForTesting
public List<Track> getAllTracks() { public List<Track> getAllTracks() {
@@ -241,9 +233,6 @@ public class ContentProviderUtils {
return tracks; return tracks;
} }
/**
* Gets the last track or null.
*/
public Track getLastTrack() { public Track getLastTrack() {
try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) { try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) {
// Using the same order as shown in the track list // Using the same order as shown in the track list
@@ -255,10 +244,8 @@ public class ContentProviderUtils {
} }
/** /**
* Gets a track by a track id or null
* Note that the returned track doesn't have any track points attached.
*
* @param trackId the track id. * @param trackId the track id.
* @return the track doesn't have any trackPoints attached
*/ */
public Track getTrack(long trackId) { public Track getTrack(long trackId) {
if (trackId < 0) { if (trackId < 0) {
@@ -286,7 +273,7 @@ public class ContentProviderUtils {
/** /**
* Inserts a track. * Inserts a track.
* NOTE: This doesn't insert any track points. * NOTE: This doesn't insert any trackPoints.
* *
* @param track the track * @param track the track
* @return the content provider URI of the inserted track. * @return the content provider URI of the inserted track.
@@ -297,13 +284,12 @@ public class ContentProviderUtils {
/** /**
* Updates a track. * Updates a track.
* NOTE: This doesn't update any track points. * NOTE: This doesn't update any trackPoints.
* *
* @param track the track * @param track the track
*/ */
public void updateTrack(Track track) { public void updateTrack(Track track) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.getId())});
TracksColumns._ID + "=?", new String[]{Long.toString(track.getId())});
} }
private ContentValues createContentValues(Track track) { private ContentValues createContentValues(Track track) {
@@ -407,14 +393,6 @@ public class ContentProviderUtils {
return waypoint; return waypoint;
} }
/**
* Deletes a waypoint.
* If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
*/
public void deleteWaypoint(long waypointId) { public void deleteWaypoint(long waypointId) {
final Waypoint waypoint = getWaypoint(waypointId); final Waypoint waypoint = getWaypoint(waypointId);
if (waypoint != null && waypoint.hasPhoto()) { if (waypoint != null && waypoint.hasPhoto()) {
@@ -432,10 +410,10 @@ public class ContentProviderUtils {
} }
/** /**
* Gets the next waypoint number for a type. * Gets the next waypoint number.
* Returns -1 if not able to get the next waypoint number.
* *
* @param trackId the track id * @param trackId the track id
* @return -1 if not able to get the next waypoint number.
*/ */
public int getNextWaypointNumber(long trackId) { public int getNextWaypointNumber(long trackId) {
if (trackId < 0) { if (trackId < 0) {
@@ -452,12 +430,6 @@ public class ContentProviderUtils {
return -1; return -1;
} }
/**
* Gets a waypoint from a waypoint id.
* Returns null if not found.
*
* @param waypointId the waypoint id
*/
public Waypoint getWaypoint(long waypointId) { public Waypoint getWaypoint(long waypointId) {
if (waypointId < 0) { if (waypointId < 0) {
return null; return null;
@@ -478,8 +450,7 @@ public class ContentProviderUtils {
* @param selection the selection. Can be null * @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null * @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null * @param sortOrder the sort order. Can be null
* @param maxWaypoints the maximum number of waypoints to return. -1 for no * @param maxWaypoints the maximum number of waypoints to return. -1 for no limit
* limit
*/ */
public Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints) { public Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints) {
return getWaypointCursor(null, selection, selectionArgs, sortOrder, maxWaypoints); return getWaypointCursor(null, selection, selectionArgs, sortOrder, maxWaypoints);
@@ -536,12 +507,13 @@ public class ContentProviderUtils {
String[] projection = new String[]{"count(*) AS count"}; String[] projection = new String[]{"count(*) AS count"};
String selection = WaypointsColumns.TRACKID + "=?"; String selection = WaypointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)}; String[] selectionArgs = new String[]{Long.toString(trackId)};
Cursor cursor = contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, WaypointsColumns._ID); try (Cursor cursor = contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, WaypointsColumns._ID)) {
if (cursor == null) {
cursor.moveToFirst(); return 0;
int count = cursor.getInt(0); }
cursor.close(); cursor.moveToFirst();
return count; return cursor.getInt(0);
}
} }
/** /**
@@ -582,19 +554,17 @@ public class ContentProviderUtils {
values.put(WaypointsColumns.DURATION, waypoint.getDuration()); values.put(WaypointsColumns.DURATION, waypoint.getDuration());
Location location = waypoint.getLocation(); Location location = waypoint.getLocation();
if (location != null) { values.put(WaypointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(WaypointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6)); values.put(WaypointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(WaypointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6)); values.put(WaypointsColumns.TIME, location.getTime());
values.put(WaypointsColumns.TIME, location.getTime()); if (location.hasAltitude()) {
if (location.hasAltitude()) { values.put(WaypointsColumns.ALTITUDE, location.getAltitude());
values.put(WaypointsColumns.ALTITUDE, location.getAltitude()); }
} if (location.hasAccuracy()) {
if (location.hasAccuracy()) { values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
values.put(WaypointsColumns.ACCURACY, location.getAccuracy()); }
} if (location.hasBearing()) {
if (location.hasBearing()) { values.put(WaypointsColumns.BEARING, location.getBearing());
values.put(WaypointsColumns.BEARING, location.getBearing());
}
} }
values.put(WaypointsColumns.PHOTOURL, waypoint.getPhotoUrl()); values.put(WaypointsColumns.PHOTOURL, waypoint.getPhotoUrl());
@@ -623,8 +593,8 @@ public class ContentProviderUtils {
/** /**
* Fills a {@link TrackPoint} from a cursor. * Fills a {@link TrackPoint} from a cursor.
* *
* @param cursor the cursor pointing to a trackPoint. * @param cursor the cursor pointing to a trackPoint.
* @param indexes the cached track points indexes * @param indexes the cached trackPoints indexes
*/ */
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) { static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint(); TrackPoint trackPoint = new TrackPoint();
@@ -661,20 +631,16 @@ public class ContentProviderUtils {
} }
/** /**
* Inserts multiple trackPoints points. * Inserts multiple trackPoints.
* *
* @param trackPoints an array of trackPoints * @param trackPoints an array of trackPoints
* @param length the number of trackPoints (from the beginning of the array) to * @param trackId the trackPoints id
* insert, or -1 for all of them * @return the number of trackPoints inserted
* @param trackId the trackPoints id
* @return the number of points inserted
*/ */
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, int length, long trackId) { //TODO Only used for testing and file import; might be better to replace it.
if (length == -1) { public int bulkInsertTrackPoint(TrackPoint[] trackPoints, long trackId) {
length = trackPoints.length; ContentValues[] values = new ContentValues[trackPoints.length];
} for (int i = 0; i < values.length; i++) {
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(trackPoints[i], trackId); values[i] = createContentValues(trackPoints[i], trackId);
} }
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values); return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
@@ -691,9 +657,7 @@ public class ContentProviderUtils {
if (trackId < 0) { if (trackId < 0) {
return -1L; return -1L;
} }
String selection = TrackPointsColumns._ID + "=(select min(" + TrackPointsColumns._ID String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId)}; String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) { try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -714,9 +678,7 @@ public class ContentProviderUtils {
if (trackId < 0) { if (trackId < 0) {
return -1L; return -1L;
} }
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId)}; String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) { try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -727,19 +689,17 @@ public class ContentProviderUtils {
} }
/** /**
* Gets the track point id of a location. * Gets the trackPoint id for a location.
* *
* @param trackId the track id * @param trackId the track id
* @param location the location * @param location the location
* @return track point id if the location is in the track. -1L otherwise. * @return trackPoint id if the location is in the track. -1L otherwise.
*/ */
public long getTrackPointId(long trackId, Location location) { public long getTrackPointId(long trackId, Location location) {
if (trackId < 0) { if (trackId < 0) {
return -1L; return -1L;
} }
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
+ ") from " + TrackPointsColumns.TABLE_NAME
+ " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId), Long.toString(location.getTime())}; String[] selectionArgs = new String[]{Long.toString(trackId), Long.toString(location.getTime())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) { try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -762,7 +722,7 @@ public class ContentProviderUtils {
* Creates a location cursor. The caller owns the returned cursor and is responsible for closing it. * Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
* *
* @param trackId the track id * @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore * @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit * @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest location first) * @param descending true to sort the result in descending order (latest location first)
*/ */
@@ -775,8 +735,7 @@ public class ContentProviderUtils {
String[] selectionArgs; String[] selectionArgs;
if (startTrackPointId >= 0) { if (startTrackPointId >= 0) {
String comparison = descending ? "<=" : ">="; String comparison = descending ? "<=" : ">=";
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison + "?";
+ "?";
selectionArgs = new String[]{Long.toString(trackId), Long.toString(startTrackPointId)}; selectionArgs = new String[]{Long.toString(trackId), Long.toString(startTrackPointId)};
} else { } else {
selection = TrackPointsColumns.TRACKID + "=?"; selection = TrackPointsColumns.TRACKID + "=?";
@@ -804,19 +763,17 @@ public class ContentProviderUtils {
if (trackId < 0) { if (trackId < 0) {
return null; return null;
} }
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from " String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)}; String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs); return findTrackPointBy(selection, selectionArgs);
} }
/** /**
* Inserts a track point. * Inserts a trackPoint.
* *
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
* @param trackId the track id * @param trackId the track id
* @return the content provider URI of the inserted track point * @return the content provider URI of the inserted trackPoint
*/ */
public Uri insertTrackPoint(TrackPoint trackPoint, long trackId) { public Uri insertTrackPoint(TrackPoint trackPoint, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId)); return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
@@ -826,7 +783,7 @@ public class ContentProviderUtils {
* Creates the {@link ContentValues} for a {@link TrackPoint}. * Creates the {@link ContentValues} for a {@link TrackPoint}.
* *
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
* @param trackId the track id * @param trackId the track id
*/ */
private ContentValues createContentValues(TrackPoint trackPoint, long trackId) { private ContentValues createContentValues(TrackPoint trackPoint, long trackId) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
@@ -870,7 +827,7 @@ public class ContentProviderUtils {
* When done with iteration, {@link TrackPointIterator#close()} must be called. * When done with iteration, {@link TrackPointIterator#close()} must be called.
* *
* @param trackId the track id * @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore * @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param descending true to sort the result in descending order (latest location first) * @param descending true to sort the result in descending order (latest location first)
*/ */
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending) { public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending) {
@@ -887,7 +844,7 @@ public class ContentProviderUtils {
} }
/** /**
* Gets a track point cursor. * Gets a trackPoint cursor.
* *
* @param projection the projection * @param projection the projection
* @param selection the selection * @param selection the selection
@@ -898,6 +855,24 @@ public class ContentProviderUtils {
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder); return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
} }
@VisibleForTesting
public List<TrackPoint> getTrackPoints(long trackId) {
List<TrackPoint> trackPoints = null;
try (Cursor trackPointCursor = getTrackPointCursor(trackId, -1L, -1, false)) {
if (trackPointCursor != null) {
trackPointCursor.moveToFirst();
trackPoints = new ArrayList<>(trackPointCursor.getCount());
for (int i = 0; i < trackPointCursor.getCount(); i++) {
trackPoints.add(createTrackPoint(trackPointCursor));
trackPointCursor.moveToNext();
}
}
}
return trackPoints;
}
int getDefaultCursorBatchSize() { int getDefaultCursorBatchSize() {
return defaultCursorBatchSize; return defaultCursorBatchSize;
} }
@@ -915,7 +890,7 @@ public class ContentProviderUtils {
/** /**
* Formats an array of IDs as comma separated string value * Formats an array of IDs as comma separated string value
* *
* @param ids array with IDs * @param ids array with IDs
* @return comma separated list of ids * @return comma separated list of ids
*/ */
public static String formatIdListForUri(long[] ids) { public static String formatIdListForUri(long[] ids) {
@@ -1,5 +1,7 @@
package de.dennisguse.opentracks.content.sensor; package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public final class SensorDataSet { public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN; public static final float DATA_UNAVAILABLE = Float.NaN;
@@ -11,14 +13,14 @@ public final class SensorDataSet {
private float cadence; private float cadence;
private float power; private float power;
private float batteryLevel; 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.heartRate = heartRate;
this.cadence = cadence; this.cadence = cadence;
this.power = power; this.power = power;
this.batteryLevel = batteryLevel; this.batteryLevel = batteryLevel;
this.creationTimestamp = creationTimestamp; this.time = time;
} }
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) { public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) {
@@ -63,8 +65,8 @@ public final class SensorDataSet {
return power; return power;
} }
public long getCreationTime() { public long getTime() {
return creationTimestamp; return time;
} }
/** /**
@@ -73,7 +75,7 @@ public final class SensorDataSet {
* @param maxAge the maximal age in milliseconds. * @param maxAge the maximal age in milliseconds.
*/ */
public boolean isRecent(long maxAge) { public boolean isRecent(long maxAge) {
return creationTimestamp + maxAge > System.currentTimeMillis(); return time + maxAge > System.currentTimeMillis();
} }
public boolean hasBatteryLevel() { public boolean hasBatteryLevel() {
@@ -91,4 +93,10 @@ public final class SensorDataSet {
public String getSensorAddress() { public String getSensorAddress() {
return sensorAddress; return sensorAddress;
} }
@NonNull
@Override
public String toString() {
return "time=" + getTime() + " sensor=" + getSensorAddress() + " heart=" + getHeartRate();
}
} }
@@ -49,6 +49,7 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -356,7 +357,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (lastTrackPoint != null) { if (lastTrackPoint != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastTrackPoint.getLocation()); boolean hasFix = !LocationUtils.isLocationOld(lastTrackPoint.getLocation());
boolean hasGoodFix = lastTrackPoint.hasAccuracy() && lastTrackPoint.getAccuracy() < recordingGpsAccuracy; boolean hasGoodFix = TrackPointUtils.fulfillsAccuracy(lastTrackPoint, recordingGpsAccuracy);
if (!hasFix || !hasGoodFix) { if (!hasFix || !hasGoodFix) {
lastTrackPoint = null; lastTrackPoint = null;
@@ -28,6 +28,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -446,11 +447,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
trackData.bufferedTrackPoints[trackData.numBufferedLocations] = trackPoint; trackData.bufferedTrackPoints[trackData.numBufferedTrackPoints] = trackPoint;
trackData.numBufferedLocations++; trackData.numBufferedTrackPoints++;
trackData.numberOfLocations++; trackData.numberOfLocations++;
if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) { if (trackData.numBufferedTrackPoints >= MAX_BUFFERED_LOCATIONS) {
flushLocations(trackData); flushLocations(trackData);
} }
} }
@@ -461,11 +462,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @param data the track data * @param data the track data
*/ */
private void flushLocations(TrackData data) { private void flushLocations(TrackData data) {
if (data.numBufferedLocations <= 0) { if (data.numBufferedTrackPoints <= 0) {
return; return;
} }
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.numBufferedLocations, data.track.getId()); contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(data.bufferedTrackPoints, 0, data.numBufferedTrackPoints), data.track.getId());
data.numBufferedLocations = 0; data.numBufferedTrackPoints = 0;
} }
/** /**
@@ -506,6 +507,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
final TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS]; final TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations // The number of buffered locations
int numBufferedLocations = 0; int numBufferedTrackPoints = 0;
} }
} }
@@ -33,6 +33,8 @@ import android.os.IBinder;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -59,6 +61,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils; import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils; import de.dennisguse.opentracks.util.TrackNameUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -69,13 +72,11 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/ */
public class TrackRecordingService extends Service { public class TrackRecordingService extends Service {
// Anything faster than that (in meters per second) will be considered moving.
private static final String TAG = TrackRecordingService.class.getSimpleName(); private static final String TAG = TrackRecordingService.class.getSimpleName();
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
// The following variables are set in onCreate: // The following variables are set in onCreate:
private ExecutorService executorService; @Deprecated //TODO Should not be necessary
private ExecutorService locationExecutorService; // Enforces order of location changes.
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private LocationManager locationManager; private LocationManager locationManager;
private PeriodicTaskExecutor voiceExecutor; private PeriodicTaskExecutor voiceExecutor;
@@ -139,7 +140,6 @@ public class TrackRecordingService extends Service {
private TrackStatisticsUpdater trackStatisticsUpdater; private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint; private TrackPoint lastTrackPoint;
private boolean currentSegmentHasLocation;
private boolean isIdle; private boolean isIdle;
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this); private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
@@ -147,10 +147,10 @@ public class TrackRecordingService extends Service {
@Override @Override
public void onLocationChanged(final Location location) { public void onLocationChanged(final Location location) {
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) { if (locationExecutorService == null || locationExecutorService.isShutdown() || locationExecutorService.isTerminated()) {
return; return;
} }
executorService.submit(new Runnable() { locationExecutorService.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
onLocationChangedAsync(location); onLocationChangedAsync(location);
@@ -177,7 +177,7 @@ public class TrackRecordingService extends Service {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
executorService = Executors.newSingleThreadExecutor(); locationExecutorService = Executors.newSingleThreadExecutor();
contentProviderUtils = new ContentProviderUtils(this); contentProviderUtils = new ContentProviderUtils(this);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
@@ -190,17 +190,7 @@ public class TrackRecordingService extends Service {
PreferencesUtils.register(this, sharedPreferenceChangeListener); PreferencesUtils.register(this, sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
// Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen. restartTrackAfterServiceRestart();
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
restartTrack(track);
} else {
if (isRecording()) {
Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
}
showNotification(false);
}
} }
@Override @Override
@@ -243,7 +233,7 @@ public class TrackRecordingService extends Service {
wakeLock = SystemUtils.releaseWakeLock(wakeLock); wakeLock = SystemUtils.releaseWakeLock(wakeLock);
// Shutdown the executorService last to avoid sending events to a dead executor. // Shutdown the executorService last to avoid sending events to a dead executor.
executorService.shutdown(); locationExecutorService.shutdown();
super.onDestroy(); super.onDestroy();
} }
@@ -367,8 +357,8 @@ public class TrackRecordingService extends Service {
track.getTrackStatistics().setStopTime_ms(System.currentTimeMillis()); track.getTrackStatistics().setStopTime_ms(System.currentTimeMillis());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics()); trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, TrackPoint.createPause(), null); insertTrackPoint(track, TrackPoint.createPause());
insertTrackPoint(track, TrackPoint.createResume(), null); insertTrackPoint(track, TrackPoint.createResume());
// Update shared preferences. // Update shared preferences.
updateRecordingState(trackId, false); updateRecordingState(trackId, false);
@@ -376,13 +366,26 @@ public class TrackRecordingService extends Service {
startRecording(); startRecording();
} }
private void restartTrack(Track track) { /**
* Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen.
*/
private void restartTrackAfterServiceRestart() {
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
if (isRecording()) {
Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
}
showNotification(false);
return;
}
Log.d(TAG, "Restarting track: " + track.getId()); Log.d(TAG, "Restarting track: " + track.getId());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms()); trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
trackStatisticsUpdater.addTrackPoint(locationIterator, recordingDistanceInterval); trackStatisticsUpdater.addTrackPoint(trackPointIterator, recordingDistanceInterval);
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e); Log.e(TAG, "RuntimeException", e);
} }
@@ -395,14 +398,12 @@ public class TrackRecordingService extends Service {
return; return;
} }
// Update shared preferences updateRecordingState(recordingTrackId, false);
recordingTrackPaused = false;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false);
// Update database // Update database
Track track = contentProviderUtils.getTrack(recordingTrackId); Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) { if (track != null) {
insertTrackPoint(track, TrackPoint.createResume(), null); insertTrackPoint(track, TrackPoint.createResume());
} }
startRecording(); startRecording();
@@ -416,7 +417,6 @@ public class TrackRecordingService extends Service {
remoteSensorManager = new BluetoothRemoteSensorManager(this); remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start(); remoteSensorManager.start();
lastTrackPoint = null; lastTrackPoint = null;
currentSegmentHasLocation = false;
isIdle = false; isIdle = false;
startGps(); startGps();
@@ -445,20 +445,21 @@ public class TrackRecordingService extends Service {
// Need to remember the recordingTrackId before setting it to -1L // Need to remember the recordingTrackId before setting it to -1L
long trackId = recordingTrackId; long trackId = recordingTrackId;
boolean paused = recordingTrackPaused; boolean wasPaused = recordingTrackPaused;
// Update shared preferences
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
// Update database // Update database
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
if (track != null) { if (track != null) {
// If not paused, add the last location // If not wasPaused, add the last location
if (!paused) { if (!wasPaused) {
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(trackId)); if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
// Update the recording track time // Update the recording track time
updateRecordingTrack(track); updateTrackTotalTime(track);
} }
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTrackStatistics().getStartTime_ms()); String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTrackStatistics().getStartTime_ms());
@@ -476,16 +477,14 @@ public class TrackRecordingService extends Service {
return; return;
} }
// Update shared preferences updateRecordingState(recordingTrackId, true);
recordingTrackPaused = true;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true);
// Update database // Update database
Track track = contentProviderUtils.getTrack(recordingTrackId); Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) { if (track != null) {
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(track.getId())); insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, TrackPoint.createPause(), null); insertTrackPoint(track, TrackPoint.createPause());
} }
endRecording(false); endRecording(false);
@@ -535,14 +534,19 @@ public class TrackRecordingService extends Service {
* @return the location or null * @return the location or null
*/ */
private TrackPoint getLastValidTrackPointInCurrentSegment(long trackId) { private TrackPoint getLastValidTrackPointInCurrentSegment(long trackId) {
if (!currentSegmentHasLocation) { if (!currentSegmentHasTrackPoint()) {
return null; return null;
} }
return contentProviderUtils.getLastValidTrackPoint(trackId); return contentProviderUtils.getLastValidTrackPoint(trackId);
} }
private boolean currentSegmentHasTrackPoint() {
return lastTrackPoint != null;
}
/** /**
* Updates the recording states. * Updates the recording states.
* This will inform subscribed {@link OnSharedPreferenceChangeListener}.
* *
* @param trackId the recording track id * @param trackId the recording track id
* @param paused true if the recording is paused * @param paused true if the recording is paused
@@ -574,90 +578,113 @@ public class TrackRecordingService extends Service {
TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet()); TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet());
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy); notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) { if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return; return;
} }
//TODO Necessary? TrackPointUtils.fixTime(trackPoint);
// Fix for phones that do not set the time field
if (location.getTime() == 0L) {
location.setTime(System.currentTimeMillis());
}
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId()); TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L; long idleTime = 0L;
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getLocation().getTime()) { if (TrackPointUtils.after(trackPoint, lastValidTrackPoint)) {
idleTime = location.getTime() - lastValidTrackPoint.getLocation().getTime(); idleTime = trackPoint.getTime() - lastValidTrackPoint.getTime();
} }
locationListenerPolicy.updateIdleTime(idleTime); locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener(); registerLocationListener();
} }
//Storing trackPoint
// Always insert the first segment location // Always insert the first segment location
if (!currentSegmentHasLocation) { if (!currentSegmentHasTrackPoint()) {
insertTrackPoint(track, trackPoint, null); insertTrackPoint(track, trackPoint);
currentSegmentHasLocation = true;
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
return; return;
} }
if (!LocationUtils.isValidLocation(lastValidTrackPoint.getLocation())) { if (!LocationUtils.isValidLocation(lastValidTrackPoint.getLocation())) {
// For some reason the previous first trackPoint was not stored, but currentSegmentHasLocation set true.
// Should not happen. The current segment should have a location. Just insert the current location. // Should not happen. The current segment should have a location. Just insert the current location.
insertTrackPoint(track, trackPoint, null); insertTrackPoint(track, trackPoint);
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
return; return;
} }
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint.getLocation()); double distanceToLastTrackLocation = trackPoint.distanceTo(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) { if (distanceToLastTrackLocation > maxRecordingDistance) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint); insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, TrackPoint.createPause(), null); insertTrackPoint(track, TrackPoint.createPause());
insertTrackPoint(track, trackPoint, null); insertTrackPoint(track, trackPoint);
isIdle = false; isIdle = false;
} else if (trackPoint.getSensorDataSet() != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint); lastTrackPoint = trackPoint;
insertTrackPoint(track, trackPoint, null); return;
isIdle = false;
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = true;
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else {
Log.d(TAG, "Not recording location, idle");
} }
if (trackPoint.getSensorDataSet() != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = true;
lastTrackPoint = trackPoint;
return;
}
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
Log.d(TAG, "Not recording TrackPoint, idle");
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
} }
/**
* Inserts a trackPoint if this trackPoint is different than lastValidTrackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPointIfNewer(@NonNull Track track, @NonNull TrackPoint trackPoint) {
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (TrackPointUtils.equalTime(trackPoint, lastValidTrackPoint)) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid track point time.");
return;
}
insertTrackPoint(track, trackPoint);
}
/** /**
* Inserts a trackPoint. * Inserts a trackPoint.
* *
* @param track the track * @param track the track
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
* @param lastValidTrackPoint the last valid track point, can be null
*/ */
private void insertTrackPoint(Track track, TrackPoint trackPoint, TrackPoint lastValidTrackPoint) { private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
if (trackPoint == null) {
Log.w(TAG, "Ignore insertLocation. trackPoint is null.");
return;
}
// Do not insert if inserted already
if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == trackPoint.getTime()) {
Log.w(TAG, "Ignore insertLocation. trackPoint time same as last valid track point time.");
return;
}
try { try {
contentProviderUtils.insertTrackPoint(trackPoint, track.getId()); contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
updateRecordingTrack(track); updateTrackTotalTime(track);
} catch (SQLiteException e) { } catch (SQLiteException e) {
/* /*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY). * Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -668,12 +695,13 @@ public class TrackRecordingService extends Service {
voiceExecutor.update(); voiceExecutor.update();
} }
/** /**
* Updates the recording track time. * Updates the recording track time.
* *
* @param track the track * @param track the track
*/ */
private void updateRecordingTrack(Track track) { private void updateTrackTotalTime(Track track) {
trackStatisticsUpdater.updateTime(System.currentTimeMillis()); trackStatisticsUpdater.updateTime(System.currentTimeMillis());
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics()); track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track); contentProviderUtils.updateTrack(track);
@@ -736,4 +764,22 @@ public class TrackRecordingService extends Service {
notificationManager.cancelNotification(); 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;
}
} }
@@ -5,6 +5,7 @@ import android.location.Location;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; 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 * 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(); 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 * Detaches from the track recording service. Clears the reference to the
* outer class to minimize the leak. * outer class to minimize the leak.
@@ -20,6 +20,7 @@ import android.location.Location;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/** /**
* App's service. * App's service.
@@ -112,4 +113,13 @@ public interface TrackRecordingServiceInterface {
* @return SensorDataSet object. * @return SensorDataSet object.
*/ */
SensorDataSet getSensorData(); SensorDataSet getSensorData();
/**
* Disables processing of location updates from {@link android.location.LocationManager}.
*/
@VisibleForTesting
void enableLocationExecutor(boolean enable);
@VisibleForTesting
void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager);
} }
@@ -18,19 +18,20 @@ package de.dennisguse.opentracks.stats;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import static de.dennisguse.opentracks.services.TrackRecordingService.MAX_NO_MOVEMENT_SPEED;
/** /**
* Updater for {@link TrackStatistics}. * Updater for {@link TrackStatistics}.
* For updating track {@link TrackStatistics} as new {@link TrackPoint}s are added. * For updating track {@link TrackStatistics} as new {@link TrackPoint}s are added.
* NOTE: Some of the locations represent pause/resume separator. * NOTE: Some of the locations represent pause/resume separator.
* NOTE: Has still support for segments (at the moment unused).
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
@@ -116,16 +117,16 @@ public class TrackStatisticsUpdater {
*/ */
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) { public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
// Always update time // Always update time
updateTime(trackPoint.getLocation().getTime()); updateTime(trackPoint.getTime());
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Either pause or resume marker // Either pause or resume marker
if (trackPoint.getLocation().getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) { if (trackPoint.getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) { if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) {
currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint)); currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint));
} }
trackStatistics.merge(currentSegment); trackStatistics.merge(currentSegment);
} }
currentSegment = init(trackPoint.getLocation().getTime()); currentSegment = init(trackPoint.getTime());
lastTrackPoint = null; lastTrackPoint = null;
lastMovingTrackPoint = null; lastMovingTrackPoint = null;
elevationBuffer_m.reset(); elevationBuffer_m.reset();
@@ -143,7 +144,7 @@ public class TrackStatisticsUpdater {
} }
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint); double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && (!trackPoint.hasSpeed() || trackPoint.getSpeed() < MAX_NO_MOVEMENT_SPEED)) { if (movingDistance < minRecordingDistance && !TrackPointUtils.isMoving(trackPoint)) {
speedBuffer_ms.reset(); speedBuffer_ms.reset();
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
return; return;
@@ -162,7 +163,7 @@ public class TrackStatisticsUpdater {
// Update max speed // Update max speed
if (trackPoint.hasSpeed() && lastTrackPoint.hasSpeed()) { if (trackPoint.hasSpeed() && lastTrackPoint.hasSpeed()) {
updateSpeed(trackPoint.getTime(), trackPoint.getSpeed(), lastTrackPoint.getTime(), lastTrackPoint.getSpeed()); updateSpeed(trackPoint, lastTrackPoint);
} }
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
@@ -190,23 +191,18 @@ public class TrackStatisticsUpdater {
/** /**
* Updates a speed reading while assuming the user is moving. * Updates a speed reading while assuming the user is moving.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
*/ */
@VisibleForTesting @VisibleForTesting
private void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (speed < MAX_NO_MOVEMENT_SPEED) { if (!TrackPointUtils.isMoving(trackPoint)) {
speedBuffer_ms.reset(); speedBuffer_ms.reset();
} else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) { } else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_ms.setNext(speed); speedBuffer_ms.setNext(trackPoint.getSpeed());
if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) { if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer_ms.getAverage()); currentSegment.setMaxSpeed(speedBuffer_ms.getAverage());
} }
} else { } else {
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed); Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
} }
} }
@@ -239,26 +235,21 @@ public class TrackStatisticsUpdater {
/** /**
* Returns true if the speed is valid. * Returns true if the speed is valid.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
*/ */
private boolean isValidSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last. // There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (speed == 0) { if (trackPoint.getSpeed() == 0) {
return false; return false;
} }
// The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code. // The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code.
if (Math.abs(speed - 128) < 1) { if (Math.abs(trackPoint.getSpeed() - 128) < 1) {
return false; return false;
} }
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g. // See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
long timeDifference = time - lastLocationTime; long timeDifference = trackPoint.getTime() - lastTrackPoint.getTime();
double speedDifference = Math.abs(lastLocationSpeed - speed); double speedDifference = Math.abs(lastTrackPoint.getSpeed() - trackPoint.getSpeed());
if (speedDifference > MAX_ACCELERATION * timeDifference) { if (speedDifference > MAX_ACCELERATION * timeDifference) {
return false; return false;
} }
@@ -266,8 +257,8 @@ public class TrackStatisticsUpdater {
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration. // Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_ms.isFull()) { if (speedBuffer_ms.isFull()) {
double average = speedBuffer_ms.getAverage(); double average = speedBuffer_ms.getAverage();
double diff = Math.abs(average - speed); double diff = Math.abs(average - trackPoint.getSpeed());
return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference); return (trackPoint.getSpeed() < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
} }
return true; return true;
@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Stack; import java.util.Stack;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
/** /**
@@ -90,13 +89,13 @@ public class LocationUtils {
* *
* @param tolerance in meters * @param tolerance in meters
* @param trackPoints input * @param trackPoints input
* @param decimated output
*/ */
//TODO What was it used for? Sharing data with other apps? //TODO What was it used for? Sharing data with other apps?
private static void decimate(double tolerance, List<TrackPoint> trackPoints, List<TrackPoint> decimated) { private static List<TrackPoint> decimate(double tolerance, List<TrackPoint> trackPoints) {
List<TrackPoint> decimated = new ArrayList<>();
final int n = trackPoints.size(); final int n = trackPoints.size();
if (n < 1) { if (n < 1) {
return; return null;
} }
int idx; int idx;
int maxIdx = 0; int maxIdx = 0;
@@ -142,18 +141,8 @@ public class LocationUtils {
idx++; idx++;
} }
Log.d(TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance); Log.d(TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance);
}
/** return decimated;
* Decimates the given track for the given precision.
*
* @param track a track
* @param precision desired precision in meters
*/
public static void decimate(Track track, double precision) {
ArrayList<TrackPoint> decimated = new ArrayList<>();
decimate(precision, track.getTrackPoints(), decimated);
track.setTrackPoints(decimated);
} }
/** /**
@@ -0,0 +1,56 @@
package de.dennisguse.opentracks.util;
import android.util.Log;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.TrackPoint;
public class TrackPointUtils {
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final static String TAG = TrackPointUtils.class.getSimpleName();
private TrackPointUtils() {
}
/**
* Ancient fix for phones that do not set the time in {@link android.location.Location}.
*/
//TODO Necessary?
public static void fixTime(@NonNull TrackPoint trackPoint) {
if (trackPoint.getTime() == 0L) {
Log.w(TAG, "Time of provided location was 0. Using current time.");
trackPoint.setTime(System.currentTimeMillis());
}
}
public static boolean isMoving(@NonNull TrackPoint trackPoint) {
return trackPoint.hasSpeed() && trackPoint.getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
public static boolean equalTime(TrackPoint t1, TrackPoint t2) {
if (t1 == null || t2 == null) {
return false;
}
return t1.getTime() == t2.getTime();
}
public static boolean after(TrackPoint t1, TrackPoint t2) {
if (t1 == null || t2 == null) {
return false;
}
return t1.getTime() > t2.getTime();
}
/**
* Is accuracy better than threshold?
*/
public static boolean fulfillsAccuracy(@NonNull TrackPoint trackPoint, int poorAccuracy) {
return trackPoint.hasAccuracy() && trackPoint.getAccuracy() < poorAccuracy;
}
}