Cleanup: mainly code formatting and made LocationIterator implement autoclosable.

This commit is contained in:
Dennis Guse
2019-09-07 12:23:56 +02:00
parent 025dee6821
commit 87d92d25f9
104 changed files with 1076 additions and 1526 deletions
@@ -87,6 +87,8 @@ public class BootReceiverTest {
*/ */
private boolean isServiceExisted(Context context) { private boolean isServiceExisted(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Assert.assertNotNull(activityManager);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE); List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) { for (int i = 0; i < serviceList.size(); i++) {
@@ -24,7 +24,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/** /**
* Commons utilities for creating stubs of track, location. * Commons utilities for creating stubs of track, location.
* The class will be enriched if needs more similar stubs for test.
* *
* @author Youtao Liu * @author Youtao Liu
*/ */
@@ -50,8 +49,7 @@ public class TrackStubUtils {
public static Track createTrack(int numberOfLocations) { public static Track createTrack(int numberOfLocations) {
Track track = new Track(); Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) { for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
+ i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
} }
return track; return track;
@@ -71,7 +69,7 @@ public class TrackStubUtils {
* *
* @return a SensorDataSetLocation stub. * @return a SensorDataSetLocation stub.
*/ */
public static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude, double altitude) { private static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location // Initial Location
Location loc = new Location(LOCATION_PROVIDER); Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude); loc.setLatitude(latitude);
@@ -137,8 +137,7 @@ public class CustomContentProviderUtilsImplTest {
long lastPointId = initializeTrack(trackId, numPoints); long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize); ((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints); List<Location> locations = new ArrayList<Location>(numPoints);
LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory); try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
try {
while (it.hasNext()) { while (it.hasNext()) {
Location loc = it.next(); Location loc = it.next();
Assert.assertNotNull(loc); Assert.assertNotNull(loc);
@@ -148,8 +147,6 @@ public class CustomContentProviderUtilsImplTest {
: lastPointId - numPoints + locations.size(), it.getLocationId()); : lastPointId - numPoints + locations.size(), it.getLocationId());
} }
Assert.assertEquals(numPoints, locations.size()); Assert.assertEquals(numPoints, locations.size());
} finally {
it.close();
} }
return locations; return locations;
} }
@@ -177,15 +174,12 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations. // Load all inserted locations.
long lastPointId = -1; long lastPointId = -1;
int counter = 0; int counter = 0;
LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try {
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
lastPointId = it.getLocationId(); lastPointId = it.getLocationId();
counter++; counter++;
} }
} finally {
it.close();
} }
Assert.assertTrue(numPoints == 0 || lastPointId > 0); Assert.assertTrue(numPoints == 0 || lastPointId > 0);
@@ -270,26 +264,20 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
TracksColumns._ID);
Assert.assertEquals(3, tracksCursor.getCount()); Assert.assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount()); Assert.assertEquals(20, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount()); Assert.assertEquals(1, waypointCursor.getCount());
// Delete one track. // Delete one track.
providerUtils.deleteTrack(context, trackId); providerUtils.deleteTrack(context, trackId);
// Check whether all data of a track has been deleted. // Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
TracksColumns._ID);
Assert.assertEquals(2, tracksCursor.getCount()); Assert.assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount()); Assert.assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
null, WaypointsColumns._ID);
Assert.assertEquals(0, waypointCursor.getCount()); Assert.assertEquals(0, waypointCursor.getCount());
} }
@@ -374,12 +362,12 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setType(WaypointType.STATISTICS); waypoint.setType(WaypointType.STATISTICS);
waypoint.setTripStatistics(statistics); waypoint.setTripStatistics(statistics);
Location loc = new Location("test"); Location location = new Location("test");
loc.setLatitude(22); location.setLatitude(22);
loc.setLongitude(22); location.setLongitude(22);
loc.setAccuracy((float) 1 / 100.0f); location.setAccuracy((float) 1 / 100.0f);
loc.setAltitude(2.5); location.setAltitude(2.5);
waypoint.setLocation(loc); waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock); ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock);
@@ -780,9 +768,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending.
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in descending.
*/ */
@Test @Test
public void testGetTrackPointCursor_desc() { public void testGetTrackPointCursor_desc() {
@@ -801,9 +787,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending.
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in ascending.
*/ */
@Test @Test
public void testGetTrackPointCursor_asc() { public void testGetTrackPointCursor_asc() {
@@ -849,9 +833,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)}
* in ascending.
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_asc() { public void testGetTrackPointLocationIterator_asc() {
@@ -914,8 +896,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Checks the value of a location. * Checks the value of a location.
* *
* @param i the index of this location which created in the method * @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* {@link CustomContentProviderUtilsImplTest#getTrack(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) {
@@ -49,7 +49,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, java.util.Vector, java.util.Vector, boolean)}. * Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, boolean)}.
*/ */
@Test @Test
public void testGenerateTrackDescription() { public void testGenerateTrackDescription() {
@@ -128,8 +128,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteDistance() { public void testWriteDistance() {
@@ -139,8 +138,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, String)}.
* String)}.
*/ */
@Test @Test
public void testWriteTime() { public void testWriteTime() {
@@ -150,8 +148,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteSpeed() { public void testWriteSpeed() {
@@ -161,8 +158,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteElevation() { public void testWriteElevation() {
@@ -172,8 +168,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, String)}.
* String)}.
*/ */
@Test @Test
public void testWritePace() { public void testWritePace() {
@@ -183,8 +178,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteGrade() { public void testWriteGrade() {
@@ -194,8 +188,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with a NaN.
* int, String)} with a NaN.
*/ */
@Test @Test
public void testWriteGrade_nan() { public void testWriteGrade_nan() {
@@ -205,8 +198,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with an infinite number.
* int, String)} with an infinite number.
*/ */
@Test @Test
public void testWriteGrade_infinite() { public void testWriteGrade_infinite() {
@@ -40,9 +40,8 @@ import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* Tests for {@link SearchEngine}. * Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests * These are not meant to be quality tests, but instead feature-by-feature tests.
* (in other words, they don't test the mixing of different score boostings, just * In other words, they don't test the mixing of different score boostings, just each boosting separately.
* each boosting separately)
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -50,7 +49,7 @@ import de.dennisguse.opentracks.stats.TripStatistics;
public class SearchEngineTest { public class SearchEngineTest {
private static final Location HERE = new Location("gps"); private static final Location HERE = new Location("gps");
private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000; // After OLDEST_ALLOWED_TIMESTAMP
private ContentProviderUtils providerUtils; private ContentProviderUtils providerUtils;
private SearchEngine engine; private SearchEngine engine;
@@ -26,8 +26,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/** /**
* Tests for the WaypointCreationRequest class. * Tests for the {@link WaypointCreationRequest} class.
* {@link WaypointCreationRequest}
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -52,7 +52,7 @@ public class ChartFragmentTest {
} }
/** /**
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])} * Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/ */
@Test @Test
public void testFillDataPoint_sensorIncorrect() { public void testFillDataPoint_sensorIncorrect() {
@@ -161,12 +161,11 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation2); point = fillDataPointTestHelper(sensorDataSetLocation2);
/* /*
* Computes the distance between Latitude 22 and 23. And for we set using * Computes the distance between Latitude 22 and 23.
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI. * And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/ */
float[] results = new float[4]; float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0], 0.01); Assert.assertEquals(distance1, point[0], 0.01);
@@ -176,11 +175,10 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation3); point = fillDataPointTestHelper(sensorDataSetLocation3);
/* /*
* Computes the distance between Latitude 23 and 24. And for we set using * Computes the distance between Latitude 23 and 24.
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI. * And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/ */
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1 + distance2, point[0], 0.01); Assert.assertEquals(distance1 + distance2, point[0], 0.01);
} }
@@ -210,15 +208,14 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* At first, clear old points of elevation, so give true to the second * At first, clear old points of elevation, so give true to the second parameter.
* parameter. Then only one value INITIALLONGTITUDE in buffer. * Then only one value INITIAL_ALTITUDE in buffer.
*/ */
double[] point = fillDataPointTestHelper(sensorDataSetLocation1); double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01); Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01);
/* /*
* Send another value to buffer, now there are two values, INITIALALTITUDE * Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
* and INITIALALTITUDE * 2.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2); sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
@@ -235,8 +232,8 @@ public class ChartFragmentTest {
@Test @Test
public void testFillDataPoint_speed() { public void testFillDataPoint_speed() {
/* /*
* At first, clear old points of speed, so give true to the second * At first, clear old points of speed, so give true to the second parameter.
* parameter. It will not be filled in to the speed buffer. * It will not be filled in to the speed buffer.
*/ */
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f); sensorDataSetLocation1.setSpeed(128.5f);
@@ -244,14 +241,13 @@ public class ChartFragmentTest {
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01); Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
/* /*
* Tests the logic when both metricUnits and reportSpeed are true.This * Tests the logic when both metricUnits and reportSpeed are true.
* location will be filled into speed buffer. * This location will be filled into speed buffer.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid, the value * Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
* 222 here is doesn't matter.
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130); sensorDataSetLocation2.setSpeed(130);
@@ -276,9 +272,8 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid and the speed * Add a time span here to make sure the second point and the speed is valid.
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102); sensorDataSetLocation2.setSpeed(102);
@@ -303,9 +298,8 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid and the speed * Add a time span here to make sure the second point and the speed is valid.
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102); sensorDataSetLocation2.setSpeed(102);
@@ -314,8 +308,7 @@ public class ChartFragmentTest {
} }
/** /**
* Tests the logic to get pace value when reportSpeed is false and average * Tests the logic to get pace value when reportSpeed is false and average speed is zero.
* speed is zero.
*/ */
@Test @Test
public void testFillDataPoint_pace_zeroSpeed() { public void testFillDataPoint_pace_zeroSpeed() {
@@ -49,40 +49,32 @@ import static org.mockito.Mockito.when;
*/ */
public abstract class AbstractTestFileTrackImporter { public abstract class AbstractTestFileTrackImporter {
protected static final String TRACK_NAME_0 = "blablub"; static final String TRACK_NAME_0 = "blablub";
protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer"; static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
protected static final String TRACK_NAME_1 = "another track"; static final double TRACK_LATITUDE = 48.768364;
protected static final String TRACK_DESCRIPTION_1 = "another description"; static final double TRACK_LONGITUDE = 9.177886;
static final double TRACK_ELEVATION = 324.0;
protected static final double TRACK_LATITUDE = 48.768364; static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
protected static final double TRACK_LONGITUDE = 9.177886; static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
protected static final double TRACK_ELEVATION = 324.0; static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z"; static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123"; static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123"; static final long TRACK_ID_0 = 1;
protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123"; static final long TRACK_POINT_ID_0 = 1;
static final long TRACK_POINT_ID_1 = 2;
protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US); static final long TRACK_POINT_ID_3 = 4;
protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); static final Uri TRACK_ID_0_URI = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final long TRACK_ID_0 = 1; private static final long WAYPOINT_ID_0 = 1;
protected static final long TRACK_ID_1 = 2; private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected static final long TRACK_POINT_ID_0 = 1;
protected static final long TRACK_POINT_ID_1 = 2;
protected static final long TRACK_POINT_ID_3 = 4;
protected static final long WAYPOINT_ID_0 = 1;
protected static final Uri TRACK_ID_0_URI = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final Uri TRACK_ID_1_URI = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build();
protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected final Context context = ApplicationProvider.getApplicationContext(); protected final Context context = ApplicationProvider.getApplicationContext();
static { static {
/* // We can't omit the timezones in the test, otherwise it'll use the local timezone and fail depending on where the test runner is.
* We can't omit the timezones in the test, otherwise it'll use the local
* timezone and fail depending on where the test runner is.
*/
SimpleTimeZone utc = new SimpleTimeZone(0, "UTC"); SimpleTimeZone utc = new SimpleTimeZone(0, "UTC");
DATE_FORMAT_0.setTimeZone(utc); DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc); DATE_FORMAT_1.setTimeZone(utc);
@@ -91,7 +83,7 @@ public abstract class AbstractTestFileTrackImporter {
@Mock @Mock
public ContentProviderUtils contentProviderUtils; public ContentProviderUtils contentProviderUtils;
protected Location createLocation(int index, long time) { Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER); Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index); location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index); location.setLongitude(TRACK_LONGITUDE + index);
@@ -38,7 +38,6 @@ import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* Tests for {@link GpxFileTrackImporter}. * Tests for {@link GpxFileTrackImporter}.
* *
@@ -58,6 +57,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>" + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
+ getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>" + getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>"
+ getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>"; + getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>";
private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring(0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50); private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring(0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50);
private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_LATITUDE), "1000.0"); private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_LATITUDE), "1000.0");
private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(TRACK_TIME_0, "invalid"); private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(TRACK_TIME_0, "invalid");
@@ -82,9 +82,6 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
return buffer.toString(); return buffer.toString();
} }
/**
* Tests one track with one segment.
*/
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -113,9 +110,6 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
/**
* Tests one track with two segments.
*/
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -145,9 +139,6 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
/**
* Tests one track with two segments, but no time in the track points.
*/
@Test @Test
public void testOneTrackTwoSegmentsNoTime() { public void testOneTrackTwoSegmentsNoTime() {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -171,49 +162,31 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
} }
/**
* Test an invalid xml input.
*/
@Test @Test
public void testInvalidXml() { public void testInvalidXml() {
testInvalidGpx(INVALID_XML_GPX); testInvalidGpx(INVALID_XML_GPX);
} }
/**
* Test an invalid location.
*/
@Test @Test
public void testInvalidLocation() { public void testInvalidLocation() {
testInvalidGpx(INVALID_LOCATION_GPX); testInvalidGpx(INVALID_LOCATION_GPX);
} }
/**
* Test an invalid time.
*/
@Test @Test
public void testInvalidTime() { public void testInvalidTime() {
testInvalidGpx(INVALID_TIME_GPX); testInvalidGpx(INVALID_TIME_GPX);
} }
/**
* Test an invalid altitude.
*/
@Test @Test
public void testInvalidAltitude() { public void testInvalidAltitude() {
testInvalidGpx(INVALID_ALTITUDE_GPX); testInvalidGpx(INVALID_ALTITUDE_GPX);
} }
/**
* Test an invalid latitude.
*/
@Test @Test
public void testInvalidLatitude() { public void testInvalidLatitude() {
testInvalidGpx(INVALID_LATITUDE_GPX); testInvalidGpx(INVALID_LATITUDE_GPX);
} }
/**
* Test an invalid longitude.
*/
@Test @Test
public void testInvalidLongitude() { public void testInvalidLongitude() {
testInvalidGpx(INVALID_LONGITUDE_GPX); testInvalidGpx(INVALID_LONGITUDE_GPX);
@@ -36,7 +36,6 @@ import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* Tests for {@link KmlFileTrackImporter}. * Tests for {@link KmlFileTrackImporter}.
* *
@@ -45,13 +44,11 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter { public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>"; + "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>" + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3) + getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
@@ -66,14 +63,9 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
String latitude = Double.toString(TRACK_LATITUDE + index); String latitude = Double.toString(TRACK_LATITUDE + index);
String longitude = Double.toString(TRACK_LONGITUDE + index); String longitude = Double.toString(TRACK_LONGITUDE + index);
String altitude = Double.toString(TRACK_ELEVATION + index); String altitude = Double.toString(TRACK_ELEVATION + index);
StringBuilder buffer = new StringBuilder(); return "<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " " + altitude + "</gx:coord>";
buffer.append("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " " + altitude + "</gx:coord>");
return buffer.toString();
} }
/**
* Tests one track with one segment.
*/
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -102,9 +94,6 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
/**
* Tests one track with two segments.
*/
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -40,9 +40,7 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
} }
/** /**
* Tests the * Tests the {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four situations.
* {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four
* situations.
* <ul> * <ul>
* <li>The newIdleTime is bigger than max interval.</li> * <li>The newIdleTime is bigger than max interval.</li>
* <li>The newIdleTime is between min and max interval.</li> * <li>The newIdleTime is between min and max interval.</li>
@@ -68,9 +66,9 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
} }
/** /**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance()}. * Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance_m()}.
*/ */
public void testGetMinDistance() { public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance()); assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance_m());
} }
} }
@@ -59,9 +59,7 @@ public class ControlRecordingServiceTest {
private ControlRecordingService subject; private ControlRecordingService subject;
/** /**
* Tests the start of {@link ControlRecordingService} and tests the method * Tests the start of {@link ControlRecordingService} and tests the method {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)} to start a track recording.
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to start a track recording.
*/ */
@Test @Test
public void testStartRecording() throws TimeoutException { public void testStartRecording() throws TimeoutException {
@@ -75,9 +73,7 @@ public class ControlRecordingServiceTest {
} }
/** /**
* Tests the method * Tests the method {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)} to stop a track recording.
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to stop a track recording.
*/ */
@Test @Test
public void testStopRecording() throws TimeoutException { public void testStopRecording() throws TimeoutException {
@@ -67,8 +67,9 @@ public class TrackRecordingServiceTest {
@Rule @Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private ContentProviderUtils providerUtils;
private Context context = ApplicationProvider.getApplicationContext(); private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private long trackId = Math.abs(new Random().nextLong()); private long trackId = Math.abs(new Random().nextLong());
@@ -210,7 +211,7 @@ public class TrackRecordingServiceTest {
// We don't expect to resume the previous track, because it has expired. // We don't expect to resume the previous track, because it has expired.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@@ -234,7 +235,7 @@ public class TrackRecordingServiceTest {
// We don't expect to resume the previous track, because there were already too many attempts. // We don't expect to resume the previous track, because there were already too many attempts.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@@ -256,7 +257,7 @@ public class TrackRecordingServiceTest {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@@ -268,14 +269,14 @@ public class TrackRecordingServiceTest {
// 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.
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@Test @Test
public void testStartNewTrack_noRecording() throws Exception { public void testStartNewTrack_noRecording() throws Exception {
// NOTICE: due to the way Android permissions work, if this fails, // NOTICE: due to the way Android permissions work, if this fails, uninstall the test apk then retry.
// uninstall the test apk then retry - the test must be installed *after* the app (go figure). // The test must be installed *after* the app (go figure).
// Reference: http://code.google.com/p/android/issues/detail?id=5521 // Reference: http://code.google.com/p/android/issues/detail?id=5521
BlockingBroadcastReceiver startReceiver = new BlockingBroadcastReceiver(); BlockingBroadcastReceiver startReceiver = new BlockingBroadcastReceiver();
String startAction = context.getString(R.string.track_started_broadcast_action); String startAction = context.getString(R.string.track_started_broadcast_action);
@@ -287,15 +288,15 @@ public class TrackRecordingServiceTest {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
long id = service.startNewTrack(); long newTrackId = service.startNewTrack();
Assert.assertTrue(id >= 0); Assert.assertTrue(newTrackId >= 0);
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id); Track track = providerUtils.getTrack(newTrackId);
Assert.assertNotNull(track); Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId()); Assert.assertEquals(newTrackId, track.getId());
Assert.assertEquals(PreferencesUtils.getString(context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT), track.getCategory()); Assert.assertEquals(PreferencesUtils.getString(context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT), track.getCategory());
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(newTrackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(id, service.getRecordingTrackId()); Assert.assertEquals(newTrackId, service.getRecordingTrackId());
// Verify that the start broadcast was received. // Verify that the start broadcast was received.
Assert.assertTrue(startReceiver.waitUntilReceived(1)); Assert.assertTrue(startReceiver.waitUntilReceived(1));
@@ -303,7 +304,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(1, receivedIntents.size()); Assert.assertEquals(1, receivedIntents.size());
Intent broadcastIntent = receivedIntents.get(0); Intent broadcastIntent = receivedIntents.get(0);
Assert.assertEquals(startAction, broadcastIntent.getAction()); Assert.assertEquals(startAction, broadcastIntent.getAction());
Assert.assertEquals(id, broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L)); Assert.assertEquals(newTrackId, broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L));
context.unregisterReceiver(startReceiver); context.unregisterReceiver(startReceiver);
} }
@@ -316,9 +317,8 @@ public class TrackRecordingServiceTest {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
// Starting a new track when there is a recording should just return -1L. long newTrackId = service.startNewTrack();
long newTrack = service.startNewTrack(); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(-1L, newTrack);
Assert.assertEquals(trackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(trackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(trackId, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
@@ -360,9 +360,7 @@ public class TrackRecordingServiceTest {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
/* // 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();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
@@ -407,7 +405,7 @@ public class TrackRecordingServiceTest {
// TODO: Check waypoint 2. // TODO: Check waypoint 2.
} }
private Track createDummyTrack(long id, long stopTime, boolean isRecording) { private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track(); Track dummyTrack = new Track();
dummyTrack.setId(id); dummyTrack.setId(id);
dummyTrack.setName("Dummy Track"); dummyTrack.setName("Dummy Track");
@@ -415,7 +413,6 @@ public class TrackRecordingServiceTest {
tripStatistics.setStopTime(stopTime); tripStatistics.setStopTime(stopTime);
dummyTrack.setTripStatistics(tripStatistics); dummyTrack.setTripStatistics(tripStatistics);
addTrack(dummyTrack, isRecording); addTrack(dummyTrack, isRecording);
return dummyTrack;
} }
private void addTrack(Track track, boolean isRecording) { private void addTrack(Track track, boolean isRecording) {
@@ -427,7 +424,7 @@ public class TrackRecordingServiceTest {
} }
/** /**
* Inserts a location and waits for 100ms. * Inserts a location and waits for 200ms.
*/ */
private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException { private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException {
Location location = new Location("gps"); Location location = new Location("gps");
@@ -45,9 +45,11 @@ public class TrackRecordingServiceTestLooper {
@Rule @Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private ContentProviderUtils providerUtils;
private Context context = ApplicationProvider.getApplicationContext(); private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
@BeforeClass @BeforeClass
public static void preSetUp() { public static void preSetUp() {
// Prepare looper for Android's message queue // Prepare looper for Android's message queue
@@ -93,28 +95,28 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRequiredAccuracy() throws Exception { public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500); PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_voiceFrequencyDefault() throws Exception { public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT); PreferencesUtils.setInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_voiceFrequencyByDistance() throws Exception { public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1); PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_voiceFrequencyByTime() throws Exception { public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1); PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1);
fullRecordingSession(); fullRecordingSession();
} }
@@ -122,97 +124,97 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_maxRecordingDistanceDefault() throws Exception { public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); PreferencesUtils.setInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_maxRecordingDistance() throws Exception { public void testWithProperties_maxRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50); PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingDistanceDefault() throws Exception { public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingDistance() throws Exception { public void testWithProperties_minRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2); PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_splitFrequencyDefault() throws Exception { public void testWithProperties_splitFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT); PreferencesUtils.setInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_splitFrequencyByDistance() throws Exception { public void testWithProperties_splitFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, -1); PreferencesUtils.setInt(context, R.string.split_frequency_key, -1);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_splitFrequencyByTime() throws Exception { public void testWithProperties_splitFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, 1); PreferencesUtils.setInt(context, R.string.split_frequency_key, 1);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDefault() throws Exception { public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT); PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDisabled() throws Exception { public void testWithProperties_metricUnitsDisabled() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial)); PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingIntervalDefault() throws Exception { public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); PreferencesUtils.setInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingInterval() throws Exception { public void testWithProperties_minRecordingInterval() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2); PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRequiredAccuracyDefault() throws Exception { public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT); PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
fullRecordingSession(); fullRecordingSession();
} }
@MediumTest @MediumTest
@Test @Test
public void testIntegration_completeRecordingSession() throws Exception { public void testIntegration_completeRecordingSession() throws TimeoutException {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty()); Assert.assertTrue(tracks.isEmpty());
fullRecordingSession(); fullRecordingSession();
} }
private void fullRecordingSession() throws Exception { private void fullRecordingSession() throws TimeoutException {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
@@ -30,7 +30,6 @@ import org.junit.runner.RunWith;
/** /**
* Tests for {@link AnnouncementPeriodicTaskFactory}. * Tests for {@link AnnouncementPeriodicTaskFactory}.
* These tests require Donut+ to run.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -52,8 +52,10 @@ import static org.mockito.Mockito.when;
/** /**
* Tests for {@link AnnouncementPeriodicTask}. * Tests for {@link AnnouncementPeriodicTask}.
* WARNING: I'm not responsible if your eyes start bleeding while reading this *
* code. You have been warned. It's still better than no test, though. * WARNING: I'm not responsible if your eyes start bleeding while reading this code.
* You have been warned.
* It's still better than no test, though.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -32,16 +32,13 @@ public class FileUtilsTest extends TestCase {
* Tests {@link FileUtils#getPath(String...)}. * Tests {@link FileUtils#getPath(String...)}.
*/ */
public void testBuildExternalDirectoryPath() { public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator String expectedName = Environment.getExternalStorageDirectory() + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + "c";
+ FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator
+ "c";
String dirName = FileUtils.getPath("a", "b", "c"); String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName); assertEquals(expectedName, dirName);
} }
/** /**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file is new.
* file is new.
*/ */
public void testBuildUniqueFileName_new() { public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext"); String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
@@ -49,8 +46,7 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
* file exists already.
*/ */
public void testBuildUniqueFileName_exist() { public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator // Expect "/default.prop" to exist on the phone/emulator
@@ -69,8 +65,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in * Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Chinese and Russian). Verifies that they are allowed. * Verifies that they are allowed.
*/ */
public void testSanitizeFileName_i18n() { public void testSanitizeFileName_i18n() {
String name = "您好-привет"; String name = "您好-привет";
@@ -79,8 +75,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 * Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 characters.
* characters. Verifies that they are allowed. * Verifies that they are allowed.
*/ */
public void testSanitizeFileName_special_characters() { public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] "; String name = "$%'-_@~`!(){}^#&+,;=[] ";
@@ -89,8 +85,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped * Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped characters in a row.
* characters in a row. Verifies that they are collapsed into one underscore. * Verifies that they are collapsed into one underscore.
*/ */
public void testSanitizeFileName_collapse() { public void testSanitizeFileName_collapse() {
String name = "hello//there"; String name = "hello//there";
@@ -99,8 +95,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#truncateFileName(File, String, String)}. Verifies * Tests {@link FileUtils#truncateFileName(File, String, String)}.
* the a long file name is truncated. * Verifies the a long file name is truncated.
*/ */
public void testTruncateFileName() { public void testTruncateFileName() {
File directory = new File("/dir1/dir2/"); File directory = new File("/dir1/dir2/");
@@ -115,7 +111,6 @@ public class FileUtilsTest extends TestCase {
for (int i = 0; i < truncated.length(); i++) { for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i)); assertEquals('a', truncated.charAt(i));
} }
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
new File(directory, truncated + suffix).getPath().length());
} }
} }
@@ -161,8 +161,7 @@ public class StringUtilsTest {
} }
/** /**
* Asserts the {@link StringUtils#getTime(String)} returns the expected * Asserts the {@link StringUtils#getTime(String)} returns the expected values.
* values.
* *
* @param xmlDateTime the xml date time string * @param xmlDateTime the xml date time string
* @param year the expected year * @param year the expected year
@@ -173,8 +172,7 @@ public class StringUtilsTest {
* @param second the expected second * @param second the expected second
* @param millisecond the expected milliseconds * @param millisecond the expected milliseconds
*/ */
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
int second, int millisecond) {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second); calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond); calendar.set(GregorianCalendar.MILLISECOND, millisecond);
@@ -45,8 +45,7 @@ public class TrackNameUtilsTest {
private static final Context CONTEXT = ApplicationProvider.getApplicationContext(); private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_date_local_value.
* settings_recording_track_name_date_local_value.
*/ */
@Test @Test
public void testTrackName_date_local() { public void testTrackName_date_local() {
@@ -55,8 +54,7 @@ public class TrackNameUtilsTest {
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_date_iso_8601_value.
* settings_recording_track_name_date_iso_8601_value.
*/ */
@Test @Test
public void testTrackName_date_iso_8601() { public void testTrackName_date_iso_8601() {
@@ -66,8 +64,7 @@ public class TrackNameUtilsTest {
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_number_value.
* settings_recording_track_name_number_value.
*/ */
@Test @Test
public void testTrackName_number() { public void testTrackName_number() {
@@ -25,16 +25,16 @@ import android.graphics.Path;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import java.text.NumberFormat; import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
/** /**
* This class encapsulates the meta data for one series of the chart values. * This class encapsulates the meta data for one series of the chart values.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class ChartValueSeries { class ChartValueSeries {
private static final float STROKE_WIDTH = 2f; private static final float STROKE_WIDTH = 2f;
@@ -68,8 +68,7 @@ public class ChartValueSeries {
* @param fillColor the fill color * @param fillColor the fill color
* @param strokeColor the stroke color * @param strokeColor the stroke color
*/ */
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin; this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax; this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues; this.intervalValues = intervalValues;
@@ -108,7 +107,7 @@ public class ChartValueSeries {
/** /**
* Returns true if the series is enabled. * Returns true if the series is enabled.
*/ */
public boolean isEnabled() { boolean isEnabled() {
return enabled; return enabled;
} }
@@ -117,14 +116,14 @@ public class ChartValueSeries {
* *
* @param enabled true to enable * @param enabled true to enable
*/ */
public void setEnabled(boolean enabled) { void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
/** /**
* Returns true if the series has data. * Returns true if the series has data.
*/ */
public boolean hasData() { boolean hasData() {
return extremityMonitor.hasData(); return extremityMonitor.hasData();
} }
@@ -133,14 +132,14 @@ public class ChartValueSeries {
* *
* @param value the new value * @param value the new value
*/ */
public void update(double value) { void update(double value) {
extremityMonitor.update(value); extremityMonitor.update(value);
} }
/** /**
* Gets the path. * Gets the path.
*/ */
public Path getPath() { Path getPath() {
return path; return path;
} }
@@ -149,7 +148,7 @@ public class ChartValueSeries {
* *
* @param canvas the canvas * @param canvas the canvas
*/ */
public void drawPath(Canvas canvas) { void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint); canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint); canvas.drawPath(path, strokePaint);
} }
@@ -157,7 +156,7 @@ public class ChartValueSeries {
/** /**
* Updates the y axis dimension. * Updates the y axis dimension.
*/ */
public void updateDimension() { void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0; double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0; double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin); min = Math.max(min, absoluteMin);
@@ -204,14 +203,15 @@ public class ChartValueSeries {
/** /**
* Gets the interval value. * Gets the interval value.
*/ */
public int getInterval() { @VisibleForTesting
int getInterval() {
return interval; return interval;
} }
/** /**
* Gets the min marker value. * Gets the min marker value.
*/ */
public int getMinMarkerValue() { int getMinMarkerValue() {
return minMarkerValue; return minMarkerValue;
} }
@@ -226,28 +226,28 @@ public class ChartValueSeries {
/** /**
* Gets the title id. * Gets the title id.
*/ */
public int getTitleId(boolean metricUnits) { int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId; return metricUnits ? metricTitleId : imperialTitleId;
} }
/** /**
* Gets the title paint. * Gets the title paint.
*/ */
public Paint getTitlePaint() { Paint getTitlePaint() {
return titlePaint; return titlePaint;
} }
/** /**
* Gets the marker paint. * Gets the marker paint.
*/ */
public Paint getMarkerPaint() { Paint getMarkerPaint() {
return markerPaint; return markerPaint;
} }
/** /**
* Gets the largest marker. * Gets the largest marker.
*/ */
public String getLargestMarker() { String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue()); String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue()); String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker; return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
@@ -258,7 +258,7 @@ public class ChartValueSeries {
* *
* @param value the value * @param value the value
*/ */
public String formatMarker(int value) { String formatMarker(int value) {
return numberFormat.format(value); return numberFormat.format(value);
} }
} }
@@ -565,7 +565,7 @@ public class ChartView extends View {
* @param canvas the canvas * @param canvas the canvas
*/ */
private void drawSeriesTitles(Canvas canvas) { private void drawSeriesTitles(Canvas canvas) {
int[] titleDimensions = getTitleDimenions(); int[] titleDimensions = getTitleDimensions();
int lines = titleDimensions[0]; int lines = titleDimensions[0];
int lineHeight = titleDimensions[1]; int lineHeight = titleDimensions[1];
int count = 0; int count = 0;
@@ -583,10 +583,10 @@ public class ChartView extends View {
} }
/** /**
* Gets the title dimensions. Returns an array of 2 integers, first element is * Gets the title dimensions.
* the number of lines and the second element is the line height. * Returns an array of 2 integers, first element is the number of lines and the second element is the line height.
*/ */
private int[] getTitleDimenions() { private int[] getTitleDimensions() {
int lines = 0; int lines = 0;
int lineHeight = 0; int lineHeight = 0;
for (int i = 0; i < series.length; i++) { for (int i = 0; i < series.length; i++) {
@@ -763,8 +763,7 @@ public class ChartView extends View {
} }
/** /**
* Updates paths. The path needs to be updated any time after the data or the * Updates paths. The path needs to be updated any time after the data or the dimensions change.
* dimensions change.
*/ */
private void updatePaths() { private void updatePaths() {
synchronized (chartData) { synchronized (chartData) {
@@ -862,7 +861,7 @@ public class ChartView extends View {
} }
leftBorder = (int) (density * BORDER + markerLength); leftBorder = (int) (density * BORDER + markerLength);
int[] titleDimensions = getTitleDimenions(); int[] titleDimensions = getTitleDimensions();
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer)); topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel()); Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label // border + x axis marker + spacer + .5 x axis label
@@ -1,33 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
/**
* Common constants.
*
* @author Leif Hendrik Wilden
*/
public class Constants {
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
private Constants() {
}
}
@@ -25,13 +25,12 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment; import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/** /**
* An activity to display marker detail info. * An activity to display marker detail info.
@@ -41,6 +40,7 @@ import java.util.ArrayList;
public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller { public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName(); private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private ArrayList<Long> markerIds; private ArrayList<Long> markerIds;
@@ -108,7 +108,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
*/ */
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter { private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) { MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager); super(fragmentManager);
} }
@@ -41,6 +41,7 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerEditActivity.class.getSimpleName(); private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId; private long trackId;
private long markerId; private long markerId;
@@ -127,8 +128,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) { if (newMarker) {
statisticsSection.setVisibility(View.GONE); statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE); waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
: ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) { if (nextWaypointNumber == -1) {
nextWaypointNumber = 0; nextWaypointNumber = 0;
} }
@@ -65,8 +65,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL, WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE}; WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/* /*
@@ -87,16 +87,25 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private static final String PHOTO_URL_FIELD = "photoUrl"; private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude"; private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude"; private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId"; private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId"; private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine; private SearchEngine searchEngine;
private ArrayAdapter<Map<String, Object>> arrayAdapter; private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true; private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
// Callback when an item is selected in the contextual action mode // Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override @Override
@@ -132,9 +141,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
return handleContextItem(itemId, positions); return handleContextItem(itemId, positions);
} }
}; };
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private final OnSharedPreferenceChangeListener private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) { if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
@@ -171,12 +179,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
@NonNull @NonNull
@Override @Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view; View view = convertView;
if (convertView == null) { if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false); view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
} }
Map<String, Object> resultMap = getItem(position); Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD); boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD); boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
@@ -207,13 +214,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
Map<String, Object> item = arrayAdapter.getItem(position); Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD); Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD); Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class);
if (markerId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) intent = intent.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else { } else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) intent = intent.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
} }
startActivity(intent); startActivity(intent);
} }
@@ -391,13 +396,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly. //TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size()); ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) { for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<>();
if (result.track != null) { if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap); output.add(prepareTrackForDisplay(result.track));
} else { } else {
prepareMarkerForDisplay(result.waypoint, resultMap); output.add(prepareMarkerForDisplay(result.waypoint));
} }
output.add(resultMap);
} }
return output; return output;
} }
@@ -405,11 +408,12 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
/** /**
* Prepares a marker for display by filling in a result map. * Prepares a marker for display by filling in a result map.
* *
* @param waypoint the marker * @param waypoint the marker
* @param resultMap the result map * @return the result map
*/ */
@Deprecated private Map<String, Object> prepareMarkerForDisplay(Waypoint waypoint) {
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) { Map<String, Object> resultMap = new HashMap<>();
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the search. //TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the search.
String trackName = null; String trackName = null;
long trackId = waypoint.getTrackId(); long trackId = waypoint.getTrackId();
@@ -440,17 +444,20 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude()); resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude()); resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude());
return resultMap;
} }
/** /**
* Prepares a track for display by filling in a result map. * Prepares a track for display by filling in a result map.
* *
* @param track the track * @param track the track
* @param resultMap the result map * @return the result map
*/ */
@Deprecated private Map<String, Object> prepareTrackForDisplay(Track track) {
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) { Map<String, Object> resultMap = new HashMap<>();
TripStatistics tripStatitics = track.getTripStatistics();
TripStatistics tripStatistics = track.getTripStatistics();
String icon = track.getIcon(); String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory(); String category = icon != null && !icon.equals("") ? null : track.getCategory();
@@ -459,15 +466,17 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon)); resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track); resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName()); resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime())); resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits)); resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId())); resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime()); resultMap.put(START_TIME_FIELD, tripStatistics.getStartTime());
resultMap.put(CATEGORY_FIELD, category); resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription()); resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null); resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId()); resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null); resultMap.put(MARKER_ID_FIELD, null);
return resultMap;
} }
@Override @Override
@@ -6,6 +6,12 @@ import android.util.Log;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Code that is executed when the application starts.
* <p>
* NOTE: How often actual application startup happens depends on the OS.
* Not every start of an activity will trigger this.
*/
public class Startup extends Application { public class Startup extends Application {
private final static String TAG = Startup.class.getSimpleName(); private final static String TAG = Startup.class.getSimpleName();
@@ -97,9 +97,8 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
/* /*
* Unfortunately when TabHost changes the current tab, it kindly also takes * Unfortunately when TabHost changes the current tab, it kindly also takes care of putting focus on it when not in touch mode.
* care of putting focus on it when not in touch mode. The jerk. This hack * The jerk. This hack tries to prevent this from pulling focus out of our ViewPager.
* tries to prevent this from pulling focus out of our ViewPager.
*/ */
TabWidget tabWidget = tabHost.getTabWidget(); TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability(); int oldFocusability = tabWidget.getDescendantFocusability();
@@ -122,15 +121,14 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Class<?> clss; private final Class<?> clss;
private final Bundle bundle; private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) { TabInfo(Class<?> clss, Bundle bundle) {
this.clss = clss; this.clss = clss;
this.bundle = bunlde; this.bundle = bundle;
} }
} }
/** /**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the * A dummy {@link TabContentFactory} that creates an empty view to satisfy the {@link TabHost} API.
* {@link TabHost} API.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
@@ -138,7 +136,7 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Context context; private final Context context;
public DummyTabFactory(Context context) { DummyTabFactory(Context context) {
this.context = context; this.context = context;
} }
@@ -27,7 +27,6 @@ import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
/** /**
* Track controller for record, pause, resume, and stop. * Track controller for record, pause, resume, and stop.
@@ -67,7 +66,7 @@ public class TrackController {
} }
}; };
public TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) { TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
this.activity = activity; this.activity = activity;
this.trackRecordingServiceConnection = trackRecordingServiceConnection; this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow; this.alwaysShow = alwaysShow;
@@ -121,21 +120,21 @@ public class TrackController {
} }
} }
public void onResume(boolean recording, boolean paused) { void onResume(boolean recording, boolean paused) {
isResumed = true; isResumed = true;
update(recording, paused); update(recording, paused);
} }
public void onPause() { void onPause() {
isResumed = false; isResumed = false;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
} }
public void hide() { void hide() {
containerView.setVisibility(View.GONE); containerView.setVisibility(View.GONE);
} }
public void show() { void show() {
containerView.setVisibility(View.VISIBLE); containerView.setVisibility(View.VISIBLE);
} }
@@ -71,7 +71,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackDetailActivity.class.getSimpleName(); private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key"; private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key"; private static final String HAS_PHOTO_KEY = "has_photo_key";
@@ -101,8 +103,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
// After binding changes (is available), update the total time in // After binding changes (is available), update the total time in trackController.
// trackController.
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -153,12 +154,14 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
}); });
} }
}; };
private MenuItem insertMarkerMenuItem; private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem; private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem; private MenuItem markerListMenuItem;
private MenuItem shareMenuItem; private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem; private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem; private MenuItem splitFrequencyMenuItem;
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -37,8 +37,7 @@ import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
/** /**
* An activity that let's the user see and edit the user editable track meta * An activity that let's the user see and edit the user editable track meta data such as track name, activity type, and track description.
* data such as track name, activity type, and track description.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
@@ -48,6 +47,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
public static final String EXTRA_NEW_TRACK = "new_track"; public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName(); private static final String TAG = TrackEditActivity.class.getSimpleName();
private static final String ICON_VALUE_KEY = "icon_value_key"; private static final String ICON_VALUE_KEY = "icon_value_key";
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -55,7 +55,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private Track track; private Track track;
private String iconValue; private String iconValue;
private EditText name; private EditText nameEditText;
private AutoCompleteTextView activityType; private AutoCompleteTextView activityType;
private Spinner activityTypeIcon; private Spinner activityTypeIcon;
private EditText description; private EditText description;
@@ -80,14 +80,13 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return; return;
} }
name = findViewById(R.id.track_edit_name); nameEditText = findViewById(R.id.track_edit_name);
name.setText(track.getName()); nameEditText.setText(track.getName());
activityType = findViewById(R.id.track_edit_activity_type); activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory()); activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource( ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter); activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() { activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
@@ -139,11 +138,11 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
description = findViewById(R.id.track_edit_description); description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription()); description.setText(track.getDescription());
Button save = findViewById(R.id.track_edit_save); Button saveButton = findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() { saveButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, name.getText().toString(), TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
activityType.getText().toString(), description.getText().toString(), activityType.getText().toString(), description.getText().toString(),
contentProviderUtils); contentProviderUtils);
finish(); finish();
@@ -76,9 +76,11 @@ import de.dennisguse.opentracks.util.TrackUtils;
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName(); private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME, private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON}; TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON};
// The following are set in onCreate // The following are set in onCreate
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
@@ -86,6 +88,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private TrackController trackController; private TrackController trackController;
private ListView listView; private ListView listView;
private ResourceCursorAdapter resourceCursorAdapter; private ResourceCursorAdapter resourceCursorAdapter;
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() { private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override @Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
@@ -103,9 +106,11 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
resourceCursorAdapter.swapCursor(null); resourceCursorAdapter.swapCursor(null);
} }
}; };
// Preferences // Preferences
private boolean metricUnits = true; private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Callback when an item is selected in the contextual action mode // Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -126,7 +131,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return handleContextItem(itemId, ids); return handleContextItem(itemId, ids);
} }
}; };
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/* /*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class. * Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected. * Anonymous inner class will get garbage collected.
@@ -165,11 +172,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
} }
}; };
// Menu items // Menu items
private MenuItem searchMenuItem; private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem; private MenuItem startGpsMenuItem;
private MenuItem aggregatedStatisticsMenuItem; private MenuItem aggregatedStatisticsMenuItem;
private MenuItem deleteAllMenuItem; private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() { private final OnClickListener stopListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -178,15 +187,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
TrackListActivity.this, trackRecordingServiceConnection, true); TrackListActivity.this, trackRecordingServiceConnection, true);
} }
}; };
private boolean startGps = false; // true to start gps private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording private boolean startNewRecording = false; // true to start a new recording
// Callback when the trackRecordingServiceConnection binding changes. // Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
/* /*
* After binding changes (e.g., becomes available), update the total time * After binding changes (e.g., becomes available), update the total time in trackController.
* in trackController.
*/ */
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
@@ -212,8 +223,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class) Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
Toast.makeText( Toast.makeText(TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show(); .show();
} }
if (startGps) { if (startGps) {
@@ -222,6 +232,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
} }
}; };
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
@@ -255,14 +266,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener); trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed // Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE); SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() { if (searchManager != null) {
@Override searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
public void onDismiss() { @Override
trackController.show(); public void onDismiss() {
} trackController.show();
}); }
});
}
listView = findViewById(R.id.track_list); listView = findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view)); listView.setEmptyView(findViewById(R.id.track_list_empty_view));
@@ -274,6 +288,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(newIntent); startActivity(newIntent);
} }
}); });
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) { resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
@@ -395,7 +410,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.track_list_start_gps: case R.id.track_list_start_gps:
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent); startActivity(intent);
} else { } else {
@@ -8,6 +8,9 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
/**
* Wraps a {@link ContentResolver} and provides access as {@link IContentResolver}.
*/
public class ContentResolverWrapper implements IContentResolver { public class ContentResolverWrapper implements IContentResolver {
private ContentResolver contentResolver; private ContentResolver contentResolver;
@@ -35,6 +35,11 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
*/ */
public interface ContentProviderUtils { public interface ContentProviderUtils {
/**
* Maximum number of waypoints that will be loaded at one time.
*/
int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/** /**
* 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.
*/ */
@@ -56,7 +61,8 @@ public interface ContentProviderUtils {
}; };
/** /**
* Clears a track. Removes waypoints and trackpoints. Only keeps the track id. * Clears a track: removes waypoints and trackpoints.
* Only keeps the track id.
* *
* @param trackId the track id * @param trackId the track id
*/ */
@@ -82,20 +88,19 @@ public interface ContentProviderUtils {
void deleteTrack(Context context, long trackId); void deleteTrack(Context context, long trackId);
/** /**
* Gets all the tracks. If no track exists, an empty list is returned. * Gets all the tracks.
* <p> * If no track exists, an empty list is returned.
* Note that the returned tracks do not have any track points attached. * NOTE: the returned tracks do not have any track points attached.
*/ */
List<Track> getAllTracks(); List<Track> getAllTracks();
/** /**
* Gets the last track. Returns null if doesn't exist. * Gets the last track or null.
*/ */
Track getLastTrack(); Track getLastTrack();
/** /**
* Gets a track by a track id. Returns null if not found. * Gets a track by a track id or null
* <p>
* Note that the returned track doesn't have any track points attached. * Note that the returned track doesn't have any track points attached.
* *
* @param trackId the track id. * @param trackId the track id.
@@ -103,8 +108,8 @@ public interface ContentProviderUtils {
Track getTrack(long trackId); Track getTrack(long trackId);
/** /**
* Gets a track cursor. The caller owns the returned cursor and is responsible * Gets a track cursor.
* for closing it. * The caller owns the returned cursor and is responsible for closing it.
* *
* @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
@@ -114,8 +119,7 @@ public interface ContentProviderUtils {
/** /**
* Inserts a track. * Inserts a track.
* <p> * NOTE: This doesn't insert any track points.
* Note: This doesn't insert any track points.
* *
* @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.
@@ -124,8 +128,7 @@ public interface ContentProviderUtils {
/** /**
* Updates a track. * Updates a track.
* <p> * NOTE: This doesn't update any track points.
* Note: This doesn't update any track points.
* *
* @param track the track * @param track the track
*/ */
@@ -139,19 +142,19 @@ public interface ContentProviderUtils {
Waypoint createWaypoint(Cursor cursor); Waypoint createWaypoint(Cursor cursor);
/** /**
* Deletes a waypoint. If deleting a statistics waypoint, this will also * Deletes a waypoint.
* correct the next statistics waypoint after the deleted one to reflect the * If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* deletion. The generator is used to update the next statistics waypoint. * The generator is used to update the next statistics waypoint.
* *
* @param waypointId the waypoint id * @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for * @param descriptionGenerator the description generator. Can be null for waypoint marker
* waypoint marker
*/ */
void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator); void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator);
/** /**
* Gets the first waypoint id for a track. The first waypoint is special as it * Gets the first waypoint id for a track.
* contains the stats for the track. Returns -1L if it doesn't exist. * The first waypoint is special as it contains the stats for the track.
* Returns -1L if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
@@ -166,8 +169,8 @@ public interface ContentProviderUtils {
Waypoint getLastWaypoint(long trackId, WaypointType waypointType); Waypoint getLastWaypoint(long trackId, WaypointType waypointType);
/** /**
* Gets the next waypoint number for a type. Returns -1 if not able to get the * Gets the next waypoint number for a type.
* next waypoint number. * Returns -1 if not able to get the next waypoint number.
* *
* @param trackId the track id * @param trackId the track id
* @param waypointType the waypoint type * @param waypointType the waypoint type
@@ -175,15 +178,16 @@ public interface ContentProviderUtils {
int getNextWaypointNumber(long trackId, WaypointType waypointType); int getNextWaypointNumber(long trackId, WaypointType waypointType);
/** /**
* Gets a waypoint from a waypoint id. Returns null if not found. * Gets a waypoint from a waypoint id.
* Returns null if not found.
* *
* @param waypointId the waypoint id * @param waypointId the waypoint id
*/ */
Waypoint getWaypoint(long waypointId); Waypoint getWaypoint(long waypointId);
/** /**
* Gets a waypoint cursor. The caller owns the returned cursor and is * Gets a waypoint cursor.
* responsible for closing it. * he caller owns the returned cursor and is responsible for closing it.
* *
* @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
@@ -194,13 +198,12 @@ public interface ContentProviderUtils {
Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints); Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/** /**
* Gets a waypoint cursor for a track. The caller owns the returned cursor and * Gets a waypoint cursor for a track.
* is responsible for closing it. * The caller owns the returned cursor and is responsible for closing it.
* *
* @param trackId the track id * @param trackId the track id
* @param minWaypointId the minimum waypoint id. -1L to ignore * @param minWaypointId the minimum waypoint id. -1L to ignore
* @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
*/ */
Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints); Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints);
@@ -220,7 +223,8 @@ public interface ContentProviderUtils {
Uri insertWaypoint(Waypoint waypoint); Uri insertWaypoint(Waypoint waypoint);
/** /**
* Updates a waypoint. Returns true if successful. * Updates a waypoint.
* Returns true if successful.
* *
* @param waypoint the waypoint * @param waypoint the waypoint
*/ */
@@ -245,14 +249,16 @@ public interface ContentProviderUtils {
Location createTrackPoint(Cursor cursor); Location createTrackPoint(Cursor cursor);
/** /**
* Gets the first location id for a track. Returns -1L if it doesn't exist. * Gets the first location id for a track.
* Returns -1L if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
long getFirstTrackPointId(long trackId); long getFirstTrackPointId(long trackId);
/** /**
* Gets the last location id for a track. Returns -1L if it doesn't exist. * Gets the last location id for a track.
* Returns -1L if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
@@ -268,28 +274,23 @@ public interface ContentProviderUtils {
long getTrackPointId(long trackId, Location location); long getTrackPointId(long trackId, Location location);
/** /**
* Gets the first valid location for a track. Returns null if it doesn't * Gets the first valid location for a track.
* exist. * Returns null if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
Location getFirstValidTrackPoint(long trackId); Location getFirstValidTrackPoint(long trackId);
/** /**
* Gets the last valid location for a track. Returns null if it doesn't exist. * Gets the last valid location for a track.
* Returns null if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
Location getLastValidTrackPoint(long trackId); Location getLastValidTrackPoint(long trackId);
/** /**
* Gets the last valid location. * Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
*/
Location getLastValidTrackPoint();
/**
* 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 track point id. -1L to ignore
@@ -299,20 +300,15 @@ public interface ContentProviderUtils {
Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending); Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending);
/** /**
* Creates a new read-only iterator over a given track's points. It provides a * Creates a new read-only iterator over a given track's points.
* lightweight way of iterating over long tracks without failing due to the * It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* underlying cursor limitations. Since it's a read-only iterator, * Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* {@link Iterator#remove()} always throws * Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
* {@link UnsupportedOperationException}. Each call to * When done with iteration, {@link LocationIterator#close()} must be called.
* {@link LocationIterator#next()} may advance to the next DB record, and if
* so, the iterator calls {@link LocationFactory#createLocation()} and
* populates it with information retrieved from the record. When done with
* iteration, {@link LocationIterator#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 track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest * @param descending true to sort the result in descending order (latest location first)
* location first)
* @param locationFactory the location factory * @param locationFactory the location factory
*/ */
LocationIterator getTrackPointLocationIterator(long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory); LocationIterator getTrackPointLocationIterator(long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
@@ -327,10 +323,9 @@ public interface ContentProviderUtils {
Uri insertTrackPoint(Location location, long trackId); Uri insertTrackPoint(Location location, long trackId);
/** /**
* A lightweight wrapper around the original {@link Cursor} with a method to * A lightweight wrapper around the original {@link Cursor} with a method to clean up.
* clean up.
*/ */
interface LocationIterator extends Iterator<Location> { interface LocationIterator extends Iterator<Location>, AutoCloseable {
/** /**
* Gets the most recently retrieved track point id by {@link #next()}. * Gets the most recently retrieved track point id by {@link #next()}.
@@ -349,8 +344,8 @@ public interface ContentProviderUtils {
interface LocationFactory { interface LocationFactory {
/** /**
* Creates a new {@link Location}. An implementation can create new * Creates a new {@link Location}.
* instances or reuse existing instances for optimization. * An implementation can create new instances or reuse existing instances for optimization.
*/ */
Location createLocation(); Location createLocation();
} }
@@ -358,6 +353,8 @@ public interface ContentProviderUtils {
/** /**
* A factory which can produce instances of {@link ContentProviderUtils}, and can be overridden for testing. * A factory which can produce instances of {@link ContentProviderUtils}, and can be overridden for testing.
*/ */
//TODO Is this still used? From a quick glance, it doesn't looks like it; probably a left over from testing.
@Deprecated
class Factory { class Factory {
private static Factory instance = new Factory(); private static Factory instance = new Factory();
@@ -379,8 +376,8 @@ public interface ContentProviderUtils {
} }
/** /**
* Overrides the factory instance for testing. Don't forget to set it back * Overrides the factory instance for testing.
* to the original value after testing. * Don't forget to set it back to the original value after testing.
* *
* @param factory the factory * @param factory the factory
*/ */
@@ -394,7 +391,7 @@ public interface ContentProviderUtils {
* *
* @param context the context * @param context the context
*/ */
protected ContentProviderUtils newForContext(Context context) { private ContentProviderUtils newForContext(Context context) {
return new ContentProviderUtilsImpl(context.getContentResolver()); return new ContentProviderUtilsImpl(context.getContentResolver());
} }
} }
@@ -842,14 +842,6 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
return findTrackPointBy(selection, selectionArgs); return findTrackPointBy(selection, selectionArgs);
} }
@Override
public Location getLastValidTrackPoint() {
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.LATITUDE + "<="
+ MAX_LATITUDE + ")";
return findTrackPointBy(selection, null);
}
@Override @Override
public Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending) { public Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending) {
if (trackId < 0) { if (trackId < 0) {
@@ -35,8 +35,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
/** /**
* A {@link ContentProvider} that handles access to track points, tracks, and * A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables.
* waypoints tables.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
@@ -44,10 +43,14 @@ public abstract class CustomContentProvider extends ContentProvider {
@VisibleForTesting @VisibleForTesting
static final int DATABASE_VERSION = 23; static final int DATABASE_VERSION = 23;
@VisibleForTesting @VisibleForTesting
static final String DATABASE_NAME = "database.db"; static final String DATABASE_NAME = "database.db";
private static final String TAG = CustomContentProvider.class.getSimpleName(); private static final String TAG = CustomContentProvider.class.getSimpleName();
private final UriMatcher uriMatcher; private final UriMatcher uriMatcher;
private SQLiteDatabase db; private SQLiteDatabase db;
public CustomContentProvider() { public CustomContentProvider() {
@@ -30,12 +30,12 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class DataSource { class DataSource {
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences; private final SharedPreferences sharedPreferences;
public DataSource(Context context) { DataSource(Context context) {
contentResolver = context.getContentResolver(); contentResolver = context.getContentResolver();
sharedPreferences = PreferencesUtils.getSharedPreferences(context); sharedPreferences = PreferencesUtils.getSharedPreferences(context);
} }
@@ -46,7 +46,7 @@ public class DataSource {
* @param uri the uri * @param uri the uri
* @param observer the observer * @param observer the observer
*/ */
public void registerContentObserver(Uri uri, ContentObserver observer) { void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer); contentResolver.registerContentObserver(uri, false, observer);
} }
@@ -55,7 +55,7 @@ public class DataSource {
* *
* @param observer the observer * @param observer the observer
*/ */
public void unregisterContentObserver(ContentObserver observer) { void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer); contentResolver.unregisterContentObserver(observer);
} }
@@ -64,7 +64,7 @@ public class DataSource {
* *
* @param listener the listener * @param listener the listener
*/ */
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener); sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
} }
@@ -73,8 +73,7 @@ public class DataSource {
* *
* @param listener the listener * @param listener the listener
*/ */
public void unregisterOnSharedPreferenceChangeListener( void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
} }
} }
@@ -26,17 +26,19 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
/** /**
* Data source manager. Creates observers/listeners and manages their * Creates observers/listeners and manages their registration with {@link DataSource}.
* registration with {@link DataSource}. The observers/listeners calls * The observers/listeners calls {@link DataSourceListener} when data changes.
* {@link DataSourceListener} when data changes.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class DataSourceManager { class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName(); private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource; private final DataSource dataSource;
private final DataSourceListener dataSourceListener; private final DataSourceListener dataSourceListener;
// Registered listeners // Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class); private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler; private final Handler handler;
@@ -45,7 +47,7 @@ public class DataSourceManager {
private final TrackPointsTableObserver trackPointsTableObserver; private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener; private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) { DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener; this.dataSourceListener = dataSourceListener;
@@ -61,7 +63,7 @@ public class DataSourceManager {
* *
* @param listeners the listeners * @param listeners the listeners
*/ */
public void updateListeners(EnumSet<TrackDataType> listeners) { void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners); EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/* /*
@@ -156,7 +158,7 @@ public class DataSourceManager {
/** /**
* Unregisters all listeners with data source. * Unregisters all listeners with data source.
*/ */
public void unregisterAllListeners() { void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) { for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType); unregisterListener(trackDataType);
} }
@@ -169,7 +171,7 @@ public class DataSourceManager {
*/ */
private class TracksTableObserver extends ContentObserver { private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() { TracksTableObserver() {
super(handler); super(handler);
} }
@@ -186,7 +188,7 @@ public class DataSourceManager {
*/ */
private class WaypointsTableObserver extends ContentObserver { private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() { WaypointsTableObserver() {
super(handler); super(handler);
} }
@@ -203,7 +205,7 @@ public class DataSourceManager {
*/ */
private class TrackPointsTableObserver extends ContentObserver { private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() { TrackPointsTableObserver() {
super(handler); super(handler);
} }
@@ -18,8 +18,7 @@ package de.dennisguse.opentracks.content;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* An interface for an object that can generate descriptions of track and * An interface for an object that can generate descriptions of track and waypoint.
* waypoint.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -115,8 +115,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak); writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace // Average pace
writePace( writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace // Average moving pace
writePace(stats.getAverageMovingSpeed(), builder, writePace(stats.getAverageMovingSpeed(), builder,
@@ -132,8 +131,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak); writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain // Elevation gain
writeElevation( writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade // Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak); writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
@@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@@ -97,34 +98,35 @@ public class SearchEngine {
/** /**
* Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) * Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000)
*/ */
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L; public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/** /**
* Comparador for scored results. * Comparador for scored results.
*/ */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
new Comparator<ScoredResult>() { @Override
@Override public int compare(ScoredResult r1, ScoredResult r2) {
public int compare(ScoredResult r1, ScoredResult r2) { // Score ordering.
// Score ordering. int scoreDiff = Double.compare(r2.score, r1.score);
int scoreDiff = Double.compare(r2.score, r1.score); if (scoreDiff != 0) {
if (scoreDiff != 0) { return scoreDiff;
return scoreDiff; }
}
// Make tracks come before waypoints. // Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) { if (r1.waypoint != null && r2.track != null) {
return 1; return 1;
} else if (r1.track != null && r2.waypoint != null) { } else if (r1.track != null && r2.waypoint != null) {
return -1; return -1;
} }
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final ContentProviderUtils providerUtils; private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) { public SearchEngine(ContentProviderUtils providerUtils) {
@@ -134,6 +136,7 @@ public class SearchEngine {
/** /**
* Squashes a number by calculating 1 / log (1 + x). * Squashes a number by calculating 1 / log (1 + x).
*/ */
//TODO Why is this done?
private static double squash(double x) { private static double squash(double x) {
return 1.0 / Math.log1p(x); return 1.0 / Math.log1p(x);
} }
@@ -145,15 +148,10 @@ public class SearchEngine {
* @return a set of results, sorted according to their score * @return a set of results, sorted according to their score
*/ */
public SortedSet<ScoredResult> search(SearchQuery query) { public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
ArrayList<Waypoint> waypoints = new ArrayList<>();
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR); TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks); scoreTrackResults(retrieveTracks(query), query, scoredResults);
retrieveWaypoints(query, waypoints); scoreWaypointResults(retrieveWaypoints(query), query, scoredResults);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults; return scoredResults;
} }
@@ -161,18 +159,19 @@ public class SearchEngine {
/** /**
* Retrieves tracks matching the given query from the database. * Retrieves tracks matching the given query from the database.
* *
* @param query the query to retrieve for * @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/ */
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) { private List<Track> retrieveTracks(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
String queryLikeSelection = "%" + query.textQuery + "%"; String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[]{ String[] trackSelectionArgs = new String[]{
queryLikeSelection, queryLikeSelection,
queryLikeSelection, queryLikeSelection,
queryLikeSelection}; queryLikeSelection
};
try (Cursor cursor = providerUtils.getTrackCursor( try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
if (cursor != null) { if (cursor != null) {
tracks.ensureCapacity(cursor.getCount()); tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
@@ -180,22 +179,26 @@ public class SearchEngine {
} }
} }
} }
return tracks;
} }
/** /**
* Retrieves waypoints matching the given query from the database. * Retrieves waypoints matching the given query from the database.
* *
* @param query the query to retrieve for * @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/ */
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) { private List<Waypoint> retrieveWaypoints(SearchQuery query) {
ArrayList<Waypoint> waypoints = new ArrayList<>();
String queryLikeSelection2 = "%" + query.textQuery + "%"; String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[]{ String[] waypointSelectionArgs = new String[]{
queryLikeSelection2, queryLikeSelection2,
queryLikeSelection2, queryLikeSelection2,
queryLikeSelection2}; queryLikeSelection2
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, };
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
if (cursor != null) { if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount()); waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
@@ -206,6 +209,8 @@ public class SearchEngine {
} }
} }
} }
return waypoints;
} }
/** /**
@@ -238,7 +243,6 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory()); score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics(); TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude()); score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L; long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
@@ -281,7 +285,6 @@ public class SearchEngine {
Location location = waypoint.getLocation(); Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory()); score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude()); score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime()); score *= getTimeBoost(query, location.getTime());
@@ -302,8 +305,7 @@ public class SearchEngine {
* @param category the category of the track or waypoint * @param category the category of the track or waypoint
* @return the total boost to be applied to the result * @return the total boost to be applied to the result
*/ */
private double getTitleBoost(SearchQuery query, private double getTitleBoost(SearchQuery query, String name, String description, String category) {
String name, String description, String category) {
// Title boost: track name > description > category. // Title boost: track name > description > category.
double boost = 1.0; double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) { if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
@@ -332,8 +334,7 @@ public class SearchEngine {
} }
// Score recent tracks higher. // Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) { if (timeAgoHours > 0L) {
return squash(timeAgoHours); return squash(timeAgoHours);
} else { } else {
@@ -364,6 +365,7 @@ public class SearchEngine {
// Score tracks close to the current location higher. // Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM; double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) { if (distanceKm > 0.0) {
// Use the inverse of the amortized distance. // Use the inverse of the amortized distance.
return squash(distanceKm); return squash(distanceKm);
@@ -377,13 +379,12 @@ public class SearchEngine {
* Description of a search query, along with all contextual data needed to execute it. * Description of a search query, along with all contextual data needed to execute it.
*/ */
public static class SearchQuery { public static class SearchQuery {
public final String textQuery; final String textQuery;
public final Location currentLocation; final Location currentLocation;
public final long currentTrackId; final long currentTrackId;
public final long currentTimestamp; final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, long currentTimestamp) {
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault()); this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation; this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId; this.currentTrackId = currentTrackId;
@@ -397,7 +398,7 @@ public class SearchEngine {
public static class ScoredResult { public static class ScoredResult {
public final Track track; public final Track track;
public final Waypoint waypoint; public final Waypoint waypoint;
public final double score; final double score;
ScoredResult(Track track, double score) { ScoredResult(Track track, double score) {
this.track = track; this.track = track;
@@ -27,9 +27,9 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
/** /**
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files. * A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
* The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}. * The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}.
* <p> *
* Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}. * Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}.
* <p> *
* Explanation: * Explanation:
* Although a request is handled by a {@link android.content.ContentProvider} (with temporarily granted permission), Android's security infrastructure prevents forwarding queries to non-exported {@link android.content.ContentProvider}. * Although a request is handled by a {@link android.content.ContentProvider} (with temporarily granted permission), Android's security infrastructure prevents forwarding queries to non-exported {@link android.content.ContentProvider}.
* Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps. * Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps.
@@ -20,9 +20,10 @@ import android.location.Location;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import de.dennisguse.opentracks.stats.TripStatistics;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* A track. * A track.
@@ -43,22 +44,26 @@ public class Track implements Parcelable {
return new Track[size]; return new Track[size];
} }
}; };
private long id = -1L; private long id = -1L;
private String name = ""; private String name = "";
private String description = ""; private String description = "";
private String category = ""; private String category = "";
private long startId = -1L; private long startId = -1L;
private long stopId = -1L; private long stopId = -1L;
/* /*
* The number of location points (present even if the points themselves are * The number of location points (present even if the points themselves are not loaded)
* not loaded)
*/ */
private int numberOfPoints = 0; private int numberOfPoints = 0;
private String icon = ""; private String icon = "";
@Deprecated //TODO Is this still used or a left over from Google Drive integration?
private long modifiedTime = -1L; private long modifiedTime = -1L;
private TripStatistics tripStatistics = new TripStatistics(); private TripStatistics tripStatistics = new TripStatistics();
// Location points (which may not have been loaded) // Location points (which may not have been loaded)
private ArrayList<Location> locations = new ArrayList<>(); private List<Location> locations = new ArrayList<>();
public Track() { public Track() {
} }
@@ -190,7 +195,7 @@ public class Track implements Parcelable {
locations.add(location); locations.add(location);
} }
public ArrayList<Location> getLocations() { public List<Location> getLocations() {
return locations; return locations;
} }
@@ -35,24 +35,25 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* Track data hub. Receives data from {@link DataSource} and distributes it to * Track data hub. Receives data from {@link DataSource} and distributes it to {@link TrackDataListener} after some processing.
* {@link TrackDataListener} after some processing.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDataHub implements DataSourceListener { public class TrackDataHub implements DataSourceListener {
/** /**
* Target number of track points displayed by the map overlay. We may display * Target number of track points displayed by the map overlay.
* more than this number of points. * We may display more than this number of points.
*/ */
public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000; static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/** /**
* Maximum number of waypoints to displayed. * Maximum number of waypoints to displayed.
*/ */
@VisibleForTesting @VisibleForTesting
static final int MAX_DISPLAYED_WAYPOINTS = 128; private static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final String TAG = TrackDataHub.class.getSimpleName(); private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context; private final Context context;
private final TrackDataManager trackDataManager; private final TrackDataManager trackDataManager;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
@@ -87,8 +88,7 @@ public class TrackDataHub implements DataSourceListener {
* @param targetNumPoints the target number of points * @param targetNumPoints the target number of points
*/ */
@VisibleForTesting @VisibleForTesting
TrackDataHub(Context context, TrackDataManager trackDataManager, private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context; this.context = context;
this.trackDataManager = trackDataManager; this.trackDataManager = trackDataManager;
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
@@ -96,17 +96,11 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState(); resetSamplingState();
} }
/**
* Creates a new instance.
*/
public synchronized static TrackDataHub newInstance(Context context) { public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get( return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS); context), TARGET_DISPLAYED_TRACK_POINTS);
} }
/**
* Starts.
*/
public void start() { public void start() {
if (started) { if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start."); Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -131,9 +125,6 @@ public class TrackDataHub implements DataSourceListener {
}); });
} }
/**
* Stops.
*/
public void stop() { public void stop() {
if (!started) { if (!started) {
Log.i(TAG, "TrackDataHub not started, ignoring stop."); Log.i(TAG, "TrackDataHub not started, ignoring stop.");
@@ -151,11 +142,6 @@ public class TrackDataHub implements DataSourceListener {
dataSourceManager = null; dataSourceManager = null;
} }
/**
* Loads a track.
*
* @param trackId the track id
*/
public void loadTrack(final long trackId) { public void loadTrack(final long trackId) {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
@@ -222,8 +208,7 @@ public class TrackDataHub implements DataSourceListener {
* Returns true if the selected track is recording. * Returns true if the selected track is recording.
*/ */
public boolean isSelectedTrackRecording() { public boolean isSelectedTrackRecording() {
return selectedTrackId == recordingTrackId return selectedTrackId == recordingTrackId && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
&& recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
} }
/** /**
@@ -258,8 +243,7 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
public void run() { public void run() {
notifyTrackPointsTableUpdate( notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
} }
}); });
@@ -270,47 +254,36 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) { recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
recordingTrackPaused = PreferencesUtils.getBoolean(
context, R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(context); metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) { if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener); loadDataForListener(trackDataListener);
} }
} }
} }
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
reportSpeed = PreferencesUtils.isReportSpeed(context); reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) { if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener); loadDataForListener(trackDataListener);
} }
} }
} }
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) { recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) { if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
loadDataForListener(trackDataListener); loadDataForListener(trackDataListener);
} }
@@ -359,7 +332,7 @@ public class TrackDataHub implements DataSourceListener {
} }
/** /**
* Loads data for a listener. To be run in the {@link #handler} thread. * Loads data for a listener; to be run in the {@link #handler} thread.
* *
* @param trackDataListener the track data listener. * @param trackDataListener the track data listener.
*/ */
@@ -386,8 +359,7 @@ public class TrackDataHub implements DataSourceListener {
if (isOnlyListener) { if (isOnlyListener) {
resetSamplingState(); resetSamplingState();
} }
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.<TrackDataListener>emptySet();
: Collections.<TrackDataListener>emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners); notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
} }
@@ -397,7 +369,7 @@ public class TrackDataHub implements DataSourceListener {
} }
/** /**
* Notifies track table update. To be run in the {@link #handler} thread. * Notifies track table update; to be run in the {@link #handler} thread.
* *
* @param trackDataListeners the track data listeners to notify * @param trackDataListeners the track data listeners to notify
*/ */
@@ -412,9 +384,8 @@ public class TrackDataHub implements DataSourceListener {
} }
/** /**
* Notifies waypoint table update. Currently, reloads all the waypoints up to * Notifies waypoint table update.
* {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} * Currently, reloads all the waypoints up to {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} thread.
* thread.
* *
* @param trackDataListeners the track data listeners to notify * @param trackDataListeners the track data listeners to notify
*/ */
@@ -448,18 +419,17 @@ public class TrackDataHub implements DataSourceListener {
} }
/** /**
* Notifies track points table update. To be run in the {@link #handler} * Notifies track points table update; to be run in the {@link #handler} thread.
* thread.
* *
* @param updateSamplingState true to update the sampling state * @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners * @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners * @param sampledOutListeners the sampled-out listeners
*/ */
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) { if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
return; return;
} }
if (updateSamplingState && numLoadedPoints >= targetNumPoints) { if (updateSamplingState && numLoadedPoints >= targetNumPoints) {
// Reload and resample the track at a lower frequency. // Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points."); Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
@@ -477,11 +447,8 @@ public class TrackDataHub implements DataSourceListener {
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId); long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1; int samplingFrequency = -1;
boolean includeNextPoint = false; boolean includeNextPoint = false;
LocationIterator locationIterator = null;
try { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId,
localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -509,8 +476,7 @@ public class TrackDataHub implements DataSourceListener {
} }
} else { } else {
// Also include the last point if the selected track is not recording. // Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
|| (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false; includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) { for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location); trackDataListener.onSampledInTrackPoint(location);
@@ -525,10 +491,6 @@ public class TrackDataHub implements DataSourceListener {
localNumLoadedPoints++; localNumLoadedPoints++;
localLastSeenLocationId = locationId; localLastSeenLocationId = locationId;
} }
} finally {
if (locationIterator != null) {
locationIterator.close();
}
} }
if (updateSamplingState) { if (updateSamplingState) {
@@ -555,7 +517,7 @@ public class TrackDataHub implements DataSourceListener {
* Creates a {@link DataSource}. * Creates a {@link DataSource}.
*/ */
@VisibleForTesting @VisibleForTesting
protected DataSource newDataSource() { private DataSource newDataSource() {
return new DataSource(context); return new DataSource(context);
} }
@@ -565,7 +527,7 @@ public class TrackDataHub implements DataSourceListener {
* @param runnable the runnable * @param runnable the runnable
*/ */
@VisibleForTesting @VisibleForTesting
protected void runInHandlerThread(Runnable runnable) { private void runInHandlerThread(Runnable runnable) {
if (handler == null) { if (handler == null) {
// Use a Throwable to ensure the stack trace is logged. // Use a Throwable to ensure the stack trace is logged.
Log.d(TAG, "handler is null.", new Throwable()); Log.d(TAG, "handler is null.", new Throwable());
@@ -57,10 +57,8 @@ public interface TrackDataListener {
void onSegmentSplit(Location location); void onSegmentSplit(Location location);
/** /**
* Called when finish sending new track points. This gets called after every * Called when finish sending new track points.
* batch of calls to {@link #onSampledInTrackPoint(Location)}, * This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)}, {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit(Location)}.
* {@link #onSampledOutTrackPoint(Location)} and
* {@link #onSegmentSplit(Location)}.
*/ */
void onNewTrackPointsDone(); void onNewTrackPointsDone();
@@ -77,9 +75,8 @@ public interface TrackDataListener {
void onNewWaypoint(Waypoint waypoint); void onNewWaypoint(Waypoint waypoint);
/** /**
* Called when finish sending new waypoints. This gets called after every * Called when finish sending new waypoints.
* batch of calls to {@link #clearWaypoints()} and * This gets called after every batch of calls to {@link #clearWaypoints()} and {@link #onNewWaypoint(Waypoint)}.
* {@link #onNewWaypoint(Waypoint)}.
*/ */
void onNewWaypointsDone(); void onNewWaypointsDone();
@@ -26,24 +26,19 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* Manages register/unregister {@link TrackDataListener} and keeping the state * Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
* for each registered listener.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDataManager { class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName(); private static final String TAG = TrackDataManager.class.getSimpleName();
// Map of listener to its track data types private final Map<TrackDataListener, EnumSet<TrackDataType>> listenerToTypesMap = new HashMap<>();
private final Map<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<>();
// Map of track data type to listeners private final Map<TrackDataType, Set<TrackDataListener>> typeToListenersMap = new EnumMap<>(TrackDataType.class);
private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<>(TrackDataType.class);
public TrackDataManager() { TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) { for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>()); typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
} }
@@ -55,8 +50,7 @@ public class TrackDataManager {
* @param listener the listener * @param listener the listener
* @param trackDataTypes the track data types the listener is interested * @param trackDataTypes the track data types the listener is interested
*/ */
public void registerListener( void registerListener(TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) { if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore."); Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return; return;
@@ -72,7 +66,7 @@ public class TrackDataManager {
* *
* @param listener the listener * @param listener the listener
*/ */
public void unregisterListener(TrackDataListener listener) { void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener); EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) { if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore."); Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
@@ -88,7 +82,7 @@ public class TrackDataManager {
/** /**
* Gets the number of {@link TrackDataListener}. * Gets the number of {@link TrackDataListener}.
*/ */
public int getNumberOfListeners() { int getNumberOfListeners() {
return listenerToTypesMap.size(); return listenerToTypesMap.size();
} }
@@ -97,7 +91,7 @@ public class TrackDataManager {
* *
* @param listener the listener * @param listener the listener
*/ */
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) { EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener); return listenerToTypesMap.get(listener);
} }
@@ -106,14 +100,14 @@ public class TrackDataManager {
* *
* @param type the type * @param type the type
*/ */
public Set<TrackDataListener> getListeners(TrackDataType type) { Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type); return typeToListenersMap.get(type);
} }
/** /**
* Gets all the registered {@link TrackDataType}. * Gets all the registered {@link TrackDataType}.
*/ */
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() { EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class); EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) { for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value); types.addAll(value);
@@ -27,25 +27,9 @@ import android.provider.BaseColumns;
public interface TracksColumns extends BaseColumns { public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks"; String TABLE_NAME = "tracks";
/**
* Tracks provider uri.
*/
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks"); Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
/**
* Track content type.
*/
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track"; String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
/**
* Track id content type.
*/
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
/**
* Tracks table default sort order.
*/
String DEFAULT_SORT_ORDER = "_id"; String DEFAULT_SORT_ORDER = "_id";
// Columns // Columns
@@ -42,6 +42,7 @@ public final class Waypoint implements Parcelable {
return new Waypoint[size]; return new Waypoint[size];
} }
}; };
private long id = -1L; private long id = -1L;
private String name = ""; private String name = "";
private String description = ""; private String description = "";
@@ -60,9 +61,7 @@ public final class Waypoint implements Parcelable {
public Waypoint() { public Waypoint() {
} }
public Waypoint(String name, String description, String category, String icon, long trackId, public Waypoint(String name, String description, String category, String icon, long trackId, WaypointType type, double length, long duration, long startId, long stopId, Location location, TripStatistics tripStatistics, String photoUrl) {
WaypointType type, double length, long duration, long startId, long stopId, Location location,
TripStatistics tripStatistics, String photoUrl) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.category = category; this.category = category;
@@ -36,8 +36,10 @@ public class WaypointCreationRequest implements Parcelable {
public static final Creator CREATOR = new Creator(); public static final Creator CREATOR = new Creator();
private WaypointType type; private WaypointType type;
// true if this marker contains the track statistics // true if this marker contains the track statistics
private boolean isTrackStatistics; private boolean isTrackStatistics;
private String name; private String name;
private String category; private String category;
private String description; private String description;
@@ -29,7 +29,11 @@ import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import de.dennisguse.opentracks.ChartView; import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
@@ -43,10 +47,6 @@ import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.EnumSet;
/** /**
* A fragment to display track chart to the user. * A fragment to display track chart to the user.
@@ -79,8 +79,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private ZoomControls zoomControls; private ZoomControls zoomControls;
/** /**
* A runnable that will enable/disable zoom controls and orange pointer as * A runnable that will enable/disable zoom controls and orange pointer as appropriate and redraw.
* appropriate and redraw.
*/ */
private final Runnable updateChart = new Runnable() { private final Runnable updateChart = new Runnable() {
@Override @Override
@@ -101,15 +100,13 @@ public class ChartFragment extends Fragment implements TrackDataListener {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
/* /*
* Create a chartView here to store data thus won't need to reload all the * Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
* data on every onStart or onResume.
*/ */
chartView = new ChartView(getActivity()); chartView = new ChartView(getActivity());
} }
@Override @Override
public View onCreateView( public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false); View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls); zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() { zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@@ -131,8 +128,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout); ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams( LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams); layout.addView(chartView, layoutParams);
} }
@@ -350,9 +346,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
chartShow[index] = value; chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value); chartView.setChartValueSeriesEnabled(index, value);
return true; return true;
} else {
return false;
} }
return false;
} }
/** /**
@@ -41,7 +41,7 @@ public class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final int padding; private final int padding;
private int selected = -1; private int selected = -1;
public ChooseActivityTypeImageAdapter(Context context, List<Integer> imageIds, int width, int height, int padding) { ChooseActivityTypeImageAdapter(Context context, List<Integer> imageIds, int width, int height, int padding) {
this.context = context; this.context = context;
this.imageIds = imageIds; this.imageIds = imageIds;
this.width = width; this.width = width;
@@ -21,8 +21,8 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.DialogUtils;
/** /**
* A DialogFragment to confirm delete. * A DialogFragment to confirm delete.
@@ -38,8 +38,7 @@ public class ConfirmDeleteDialogFragment extends AbstractDialogFragment {
/** /**
* Create a new instance. * Create a new instance.
* *
* @param trackIds list of track ids to delete. To delete all, set to size 1 * @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L
* with trackIds[0] == -1L
*/ */
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) { public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@@ -65,10 +64,8 @@ public class ConfirmDeleteDialogFragment extends AbstractDialogFragment {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS); final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId; int titleId;
int messageId; int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
: R.string.track_delete_one_confirm_title; messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
: R.string.track_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog( return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() { getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override @Override
@@ -23,10 +23,10 @@ import android.os.Bundle;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl; import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.util.DialogUtils; import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.R;
/** /**
* A DialogFragment to delete marker. * A DialogFragment to delete marker.
@@ -68,10 +68,8 @@ public class DeleteMarkerDialogFragment extends AbstractDialogFragment {
titleId = R.string.generic_delete_all_confirm_title; titleId = R.string.generic_delete_all_confirm_title;
messageId = R.string.marker_delete_all_confirm_message; messageId = R.string.marker_delete_all_confirm_message;
} else { } else {
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.marker_delete_one_confirm_title;
: R.string.marker_delete_one_confirm_title; messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
: R.string.marker_delete_one_confirm_message;
} }
return DialogUtils.createConfirmationDialog( return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() { fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
@@ -96,7 +96,7 @@ public class FileTypeDialogFragment extends AbstractDialogFragment {
onDismissed(); onDismissed();
} }
protected void onDismissed() { private void onDismissed() {
dismiss(); dismiss();
caller.onDismissed(); caller.onDismissed();
} }
@@ -23,9 +23,9 @@ import android.os.Bundle;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.R;
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.R;
/** /**
* A DialogFragment to configure frequency. * A DialogFragment to configure frequency.
@@ -82,8 +82,8 @@ public class FrequencyDialogFragment extends AbstractDialogFragment {
} }
/** /**
* Gets the list index for a frequency value. Returns 0 if the value is not on * Gets the list index for a frequency value.
* the list. * Returns 0 if the value is not on the list.
*/ */
private int getListIndex(int frequencyValue) { private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values); String[] values = getResources().getStringArray(R.array.frequency_values);
@@ -69,6 +69,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private Location lastLocation = null; private Location lastLocation = null;
private TripStatistics lastTripStatistics = null; private TripStatistics lastTripStatistics = null;
private SensorDataSet sensorDataSet = null; private SensorDataSet sensorDataSet = null;
private final Runnable updateUIeachSecond = new Runnable() { private final Runnable updateUIeachSecond = new Runnable() {
public void run() { public void run() {
if (isResumed() && isSelectedTrackRecording()) { if (isResumed() && isSelectedTrackRecording()) {
@@ -38,8 +38,8 @@ import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
/** /**
* An activity for saving tracks to the external storage. If saving a specific * An activity for saving tracks to the external storage.
* track, option to save it to a temp directory and play the track afterward. * If saving a specific track, option to save it to a temp directory and play the track afterward.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -53,13 +53,11 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
private String directoryDisplayName; private String directoryDisplayName;
private ExportAsyncTask exportAsyncTask; private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
// the number of tracks successfully saved private int processedTrackCount;
private int successCount; private int totalTrackCount;
// the number of tracks to save
private int totalCount;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -144,8 +142,8 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
int titleId; int titleId;
String message; String message;
String totalTracks = getResources() String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount); .getQuantityString(R.plurals.tracks, totalTrackCount, totalTrackCount);
if (successCount == totalCount) { if (processedTrackCount == totalTrackCount) {
iconId = R.drawable.ic_dialog_success; iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title; titleId = R.string.generic_success_title;
message = getString( message = getString(
@@ -153,7 +151,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
} else { } else {
iconId = android.R.drawable.ic_dialog_alert; iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title; titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, successCount, totalTracks, message = getString(R.string.export_external_storage_error, processedTrackCount, totalTracks,
directoryDisplayName); directoryDisplayName);
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true) AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
@@ -192,8 +190,8 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
* @param aTotalCount the number of tracks to save * @param aTotalCount the number of tracks to save
*/ */
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) { public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) {
successCount = aSuccessCount; processedTrackCount = aSuccessCount;
totalCount = aTotalCount; totalTrackCount = aTotalCount;
removeDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID); showDialog(DIALOG_RESULT_ID);
} }
@@ -52,17 +52,10 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private ExportActivity exportActivity; private ExportActivity exportActivity;
private WakeLock wakeLock; private WakeLock wakeLock;
// true if the AsyncTask has completed
private boolean completed; private boolean completed;
// the number of tracks successfully saved private int processedTrackCount;
private int successCount; private int totalTrackCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
/** /**
* Creates an AsyncTask. * Creates an AsyncTask.
@@ -79,9 +72,8 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
contentProviderUtils = ContentProviderUtils.Factory.get(context); contentProviderUtils = ContentProviderUtils.Factory.get(context);
completed = false; completed = false;
successCount = 0; processedTrackCount = 0;
totalCount = 0; totalTrackCount = 0;
savedPath = null;
} }
/** /**
@@ -92,7 +84,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
public void setActivity(ExportActivity exportActivity) { public void setActivity(ExportActivity exportActivity) {
this.exportActivity = exportActivity; this.exportActivity = exportActivity;
if (completed && exportActivity != null) { if (completed && exportActivity != null) {
exportActivity.onAsyncTaskCompleted(successCount, totalCount); exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
} }
} }
@@ -132,7 +124,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected void onPostExecute(Boolean result) { protected void onPostExecute(Boolean result) {
completed = true; completed = true;
if (exportActivity != null) { if (exportActivity != null) {
exportActivity.onAsyncTaskCompleted(successCount, totalCount); exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
} }
} }
@@ -140,7 +132,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected void onCancelled() { protected void onCancelled() {
completed = true; completed = true;
if (exportActivity != null) { if (exportActivity != null) {
exportActivity.onAsyncTaskCompleted(successCount, totalCount); exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
} }
} }
@@ -172,7 +164,6 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
if (trackExporter.writeTrack(fileOutputStream)) { if (trackExporter.writeTrack(fileOutputStream)) {
savedPath = file.getAbsolutePath();
return true; return true;
} else { } else {
if (!file.delete()) { if (!file.delete()) {
@@ -198,17 +189,17 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
if (cursor == null) { if (cursor == null) {
return false; return false;
} }
totalCount = cursor.getCount(); totalTrackCount = cursor.getCount();
for (int i = 0; i < totalCount; i++) { for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) { if (isCancelled()) {
return false; return false;
} }
cursor.moveToPosition(i); cursor.moveToPosition(i);
Track track = contentProviderUtils.createTrack(cursor); Track track = contentProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[]{track})) { if (track != null && saveTracks(new Track[]{track})) {
successCount++; processedTrackCount++;
} }
publishProgress(i + 1, totalCount); publishProgress(i + 1, totalTrackCount);
} }
return true; return true;
} }
@@ -22,7 +22,6 @@ import android.util.Log;
import java.io.OutputStream; import java.io.OutputStream;
import de.dennisguse.opentracks.Constants;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator; import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
@@ -89,17 +88,13 @@ public class FileTrackExporter implements TrackExporter {
*/ */
private void writeWaypoints(Track track) throws InterruptedException { private void writeWaypoints(Track track) throws InterruptedException {
/* /*
* TODO: Stream through the waypoints in chunks. I am leaving the number of * TODO: Stream through the waypoints in chunks.
* waypoints very high which should not be a problem because we don't try to * I am leaving the number of waypoints very high which should not be a problem, because we don't try to load them into objects all at the same time.
* load them into objects all at the same time.
*/ */
boolean hasWaypoints = false; boolean hasWaypoints = false;
try (Cursor cursor = contentProviderUtils.getWaypointCursor(track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS)) { try (Cursor cursor = contentProviderUtils.getWaypointCursor(track.getId(), -1L, ContentProviderUtils.MAX_LOADED_WAYPOINTS_POINTS)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
/* // Intentionally skip first waypoint (contains statistics).
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedException(); throw new InterruptedException();
@@ -127,10 +122,8 @@ public class FileTrackExporter implements TrackExporter {
boolean isLastLocationValid = false; boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
int locationNumber = 0; int locationNumber = 0;
LocationIterator locationIterator = null;
try { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, locationFactory)) {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, locationFactory);
while (locationIterator.hasNext()) { while (locationIterator.hasNext()) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
@@ -188,10 +181,6 @@ public class FileTrackExporter implements TrackExporter {
trackWriter.writeBeginTrack(track, null); trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null); trackWriter.writeEndTrack(track, null);
} }
} finally {
if (locationIterator != null) {
locationIterator.close();
}
} }
} }
@@ -224,7 +213,7 @@ public class FileTrackExporter implements TrackExporter {
return currentLocation; return currentLocation;
} }
public void swapLocations() { void swapLocations() {
Location tempLocation = lastLocation; Location tempLocation = lastLocation;
lastLocation = currentLocation; lastLocation = currentLocation;
currentLocation = tempLocation; currentLocation = tempLocation;
@@ -32,7 +32,7 @@ import de.dennisguse.opentracks.util.StringUtils;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
//TODO Can we export Sensor.Data in GPX? //TODO Can we export SensorData in GPX?
public class GpxTrackWriter implements TrackWriter { public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US); private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -188,7 +188,6 @@ public class GpxTrackWriter implements TrackWriter {
* @param location the location * @param location the location
*/ */
private String formatLocation(Location location) { private String formatLocation(Location location) {
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\"" return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\"" + COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
} }
} }
@@ -55,23 +55,26 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Abstract class for various file track importers like {@link GpxFileTrackImporter} and * Abstract class for file track importers.
* {@link KmlFileTrackImporter}.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter { abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter {
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName(); private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
// The maximum number of buffered locations for bulk-insertion // The maximum number of buffered locations for bulk-insertion
private static final int MAX_BUFFERED_LOCATIONS = 512; private static final int MAX_BUFFERED_LOCATIONS = 512;
private final Context context; private final Context context;
private final long importTrackId; private final long importTrackId;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval; private final int recordingDistanceInterval;
private final List<Long> trackIds; private final List<Long> trackIds;
private final List<Waypoint> waypoints; private final List<Waypoint> waypoints;
// The current element content // The current element content
//TODO Should be made private and getter be used by child classes.
protected String content; protected String content;
protected String name; protected String name;
protected String description; protected String description;
@@ -82,25 +85,24 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String time; protected String time;
protected String waypointType; protected String waypointType;
protected String photoUrl; protected String photoUrl;
// The current track data // The current track data
private TrackData trackData; private TrackData trackData;
// The SAX locator to get the current line information // The SAX locator to get the current line information
private Locator locator; private Locator locator;
/** /**
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
* @param importTrackId the track id to import to. -1L to import to a new * @param importTrackId the track id to import to. -1L to import to a new track.
* track.
*/ */
AbstractFileTrackImporter( AbstractFileTrackImporter(Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
this.context = context; this.context = context;
this.importTrackId = importTrackId; this.importTrackId = importTrackId;
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getInt(context, this.recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
trackIds = new ArrayList<>(); trackIds = new ArrayList<>();
waypoints = new ArrayList<>(); waypoints = new ArrayList<>();
} }
@@ -116,11 +118,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (content == null) { if (content == null) {
content = newContent; content = newContent;
} else { } else {
/* // In 99% of the cases, a single call to this method will be made for each sequence of characters we're interested in, so we'll rarely be concatenating strings, thus not justifying the use of a StringBuilder.
* In 99% of the cases, a single call to this method will be made for each
* sequence of characters we're interested in, so we'll rarely be
* concatenating strings, thus not justifying the use of a StringBuilder.
*/
content += newContent; content += newContent;
} }
} }
@@ -164,15 +162,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
int waypointPosition = -1; int waypointPosition = -1;
Waypoint waypoint = null; Waypoint waypoint = null;
Location location = null; Location location = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater( TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
track.getTripStatistics().getStartTime()); TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
LocationIterator locationIterator = null;
try { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
while (true) { while (true) {
if (waypoint == null) { if (waypoint == null) {
@@ -198,7 +191,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
waypoint = null; waypoint = null;
} else { } else {
// The waypoint location time matches the track point time // The waypoint location time matches the track point time
if (!LocationUtils.isValidLocation(location)) { if (!LocationUtils.isValidLocation(location)) {
// Invalid location, load the next location // Invalid location, load the next location
location = null; location = null;
@@ -206,8 +198,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
// Valid location // Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude() if (location.getLatitude() == waypoint.getLocation().getLatitude() && location.getLongitude() == waypoint.getLocation().getLongitude()) {
&& location.getLongitude() == waypoint.getLocation().getLongitude()) {
// Get tripStatistics, description, and icon // Get tripStatistics, description, and icon
TripStatistics tripStatistics; TripStatistics tripStatistics;
@@ -216,8 +207,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (waypoint.getType() == WaypointType.STATISTICS) { if (waypoint.getType() == WaypointType.STATISTICS) {
tripStatistics = markerTripStatisticsUpdater.getTripStatistics(); tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(location.getTime()); markerTripStatisticsUpdater = new TripStatisticsUpdater(location.getTime());
waypointDescription = new DescriptionGeneratorImpl(context) waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(tripStatistics);
.generateWaypointDescription(tripStatistics);
icon = context.getString(R.string.marker_statistics_icon_url); icon = context.getString(R.string.marker_statistics_icon_url);
} else { } else {
tripStatistics = null; tripStatistics = null;
@@ -230,9 +220,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime(); long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
// Insert waypoint // Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration, -1L, -1L, location, tripStatistics, waypoint.getPhotoUrl());
waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration,
-1L, -1L, location, tripStatistics, waypoint.getPhotoUrl());
contentProviderUtils.insertWaypoint(newWaypoint); contentProviderUtils.insertWaypoint(newWaypoint);
} }
@@ -240,10 +228,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
waypoint = null; waypoint = null;
} }
} }
} finally {
if (locationIterator != null) {
locationIterator.close();
}
} }
} }
@@ -258,8 +242,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackId = Long.parseLong(uri.getLastPathSegment()); trackId = Long.parseLong(uri.getLastPathSegment());
} else { } else {
if (trackIds.size() > 0) { if (trackIds.size() > 0) {
throw new SAXException(createErrorMessage( throw new SAXException(createErrorMessage("Cannot import more than one track to an existing track " + importTrackId));
"Cannot import more than one track to an existing track " + importTrackId));
} }
trackId = importTrackId; trackId = importTrackId;
contentProviderUtils.clearTrack(context, trackId); contentProviderUtils.clearTrack(context, trackId);
@@ -299,13 +282,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected void onTrackSegmentStart() { protected void onTrackSegmentStart() {
trackData.numberOfSegments++; trackData.numberOfSegments++;
/* //If not the first segment, add a pause separator if there is at least one location in the last segment.
* If not the first segment, add a pause separator if there is at least one
* location in the last segment.
*/
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) { if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0, insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
trackData.lastLocationInCurrentSegment.getTime()));
} }
trackData.lastLocationInCurrentSegment = null; trackData.lastLocationInCurrentSegment = null;
} }
@@ -362,10 +341,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} else { } else {
/* /*
* We don't have a speed and bearing in GPX, make something up from the * We don't have a speed and bearing in GPX, make something up from the last two points.
* last two points. GPS points tend to have some inherent imprecision, * GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
* speed and bearing will likely be off, so the statistics for things
* like max speed will also be off.
*/ */
double duration = timeDifference * UnitConversions.MS_TO_S; double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(location) / duration; double speed = trackData.lastLocationInCurrentSegment.distanceTo(location) / duration;
@@ -379,10 +356,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) { if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
/* // If not the first segment, add a resume separator before adding the first location.
* If not the first segment, add a resume separator before adding the
* first location.
*/
insertLocation( insertLocation(
createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime())); createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
} }
@@ -441,17 +415,14 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
latitudeValue = Double.parseDouble(latitude); latitudeValue = Double.parseDouble(latitude);
longitudeValue = Double.parseDouble(longitude); longitudeValue = Double.parseDouble(longitude);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format( throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
} }
Double altitudeValue = null; Double altitudeValue = null;
if (altitude != null) { if (altitude != null) {
try { try {
altitudeValue = Double.parseDouble(altitude); altitudeValue = Double.parseDouble(altitude);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new SAXException( throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)),
e);
} }
} }
@@ -462,8 +433,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try { try {
timeValue = StringUtils.getTime(time); timeValue = StringUtils.getTime(time);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new SAXException( throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
} }
} }
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue); return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
@@ -477,8 +447,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @param altitudeValue the altitude value * @param altitudeValue the altitude value
* @param timeValue the time value * @param timeValue the time value
*/ */
private Location createLocation( private Location createLocation(double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
Location location = new Location(LocationManager.GPS_PROVIDER); Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitudeValue); location.setLatitude(latitudeValue);
location.setLongitude(longitudeValue); location.setLongitude(longitudeValue);
@@ -501,8 +470,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/ */
private void insertLocation(Location location) { private void insertLocation(Location location) {
if (trackData.tripStatisticsUpdater == null) { if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater( trackData.tripStatisticsUpdater = new TripStatisticsUpdater(location.getTime() != -1L ? location.getTime() : trackData.importTime);
location.getTime() != -1L ? location.getTime() : trackData.importTime);
} }
trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval); trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
@@ -524,8 +492,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (data.numBufferedLocations <= 0) { if (data.numBufferedLocations <= 0) {
return; return;
} }
contentProviderUtils.bulkInsertTrackPoint( contentProviderUtils.bulkInsertTrackPoint(data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0; data.numBufferedLocations = 0;
if (data.track.getStartId() == -1L) { if (data.track.getStartId() == -1L) {
data.track.setStartId(contentProviderUtils.getFirstTrackPointId(data.track.getId())); data.track.setStartId(contentProviderUtils.getFirstTrackPointId(data.track.getId()));
@@ -541,20 +508,16 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private void insertFirstWaypoint(Track track) { private void insertFirstWaypoint(Track track) {
String waypointName = context.getString(R.string.marker_split_name_format, 0); String waypointName = context.getString(R.string.marker_split_name_format, 0);
String waypointCategory = ""; String waypointCategory = "";
TripStatisticsUpdater updater = new TripStatisticsUpdater( TripStatisticsUpdater updater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
track.getTripStatistics().getStartTime());
TripStatistics tripStatistics = updater.getTripStatistics(); TripStatistics tripStatistics = updater.getTripStatistics();
String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription( String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(tripStatistics);
tripStatistics);
String icon = context.getString(R.string.marker_statistics_icon_url); String icon = context.getString(R.string.marker_statistics_icon_url);
double length = 0.0; double length = 0.0;
long duration = 0L; long duration = 0L;
Location waypointLocation = new Location(""); Location waypointLocation = new Location("");
waypointLocation.setLatitude(100); waypointLocation.setLatitude(100);
waypointLocation.setLongitude(180); waypointLocation.setLongitude(180);
Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon, Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon, track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation, tripStatistics, "");
track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation,
tripStatistics, "");
contentProviderUtils.insertWaypoint(waypoint); contentProviderUtils.insertWaypoint(waypoint);
} }
@@ -573,17 +536,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @author Jimmy Shih * @author Jimmy Shih
*/ */
private class TrackData { private class TrackData {
// The current track // The current track
Track track = new Track(); Track track = new Track();
// The number of segments processed for the current track // The number of segments processed for the current track
int numberOfSegments = 0; int numberOfSegments = 0;
/* // The last location in the current segment; Null if the current segment doesn't have a last location
* The last location in the current segment. Null if the current segment
* doesn't have a last location.
*/
Location lastLocationInCurrentSegment; Location lastLocationInCurrentSegment;
// The number of locations processed for the current track // The number of locations processed for the current track
@@ -64,8 +64,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
} }
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
throws SAXException {
switch (tag) { switch (tag) {
case TAG_WAYPOINT: case TAG_WAYPOINT:
onWaypointStart(attributes); onWaypointStart(attributes);
@@ -185,7 +184,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* On waypoint end. * On waypoint end.
*/ */
private void onWaypointEnd() throws SAXException { private void onWaypointEnd() throws SAXException {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS : WaypointType.WAYPOINT);
: WaypointType.WAYPOINT);
} }
} }
@@ -56,11 +56,9 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
// the path on the external storage to import // the path on the external storage to import
private String directoryDisplayName; private String directoryDisplayName;
// the number of files successfully imported private int importedTrackCount;
private int successCount;
// the number of files to import private int totalTrackCount;
private int totalCount;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -144,9 +142,9 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
int titleId; int titleId;
String message; String message;
String totalFiles = getResources() String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount); .getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
if (successCount == totalCount) { if (importedTrackCount == totalTrackCount) {
if (totalCount == 0) { if (totalTrackCount == 0) {
iconId = android.R.drawable.ic_dialog_info; iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.import_no_file_title; titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName); message = getString(R.string.import_no_file, directoryDisplayName);
@@ -159,7 +157,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
iconId = android.R.drawable.ic_dialog_alert; iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title; titleId = R.string.generic_error_title;
message = getString( message = getString(
R.string.import_error, successCount, totalFiles, directoryDisplayName); R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
} }
final Dialog dialog = new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId) final Dialog dialog = new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() { .setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -195,8 +193,8 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
* @param aTotalCount the number of files to import * @param aTotalCount the number of files to import
*/ */
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) { public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) {
successCount = aSuccessCount; importedTrackCount = aSuccessCount;
totalCount = aTotalCount; totalTrackCount = aTotalCount;
removeDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID); showDialog(DIALOG_RESULT_ID);
} }
@@ -53,11 +53,9 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
// true if the AsyncTask has completed // true if the AsyncTask has completed
private boolean completed; private boolean completed;
// the number of files successfully imported private int importTrackCount;
private int successCount;
// the number of files to import private int totalTrackCount;
private int totalCount;
// the last successfully imported track id // the last successfully imported track id
private long trackId; private long trackId;
@@ -76,8 +74,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
context = importActivity.getApplicationContext(); context = importActivity.getApplicationContext();
completed = false; completed = false;
successCount = 0; importTrackCount = 0;
totalCount = 0; totalTrackCount = 0;
trackId = -1L; trackId = -1L;
} }
@@ -89,7 +87,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
public void setActivity(ImportActivity importActivity) { public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity; this.importActivity = importActivity;
if (completed && importActivity != null) { if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount); importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
} }
} }
@@ -112,20 +110,20 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
} }
List<File> files = getFiles(); List<File> files = getFiles();
totalCount = files.size(); totalTrackCount = files.size();
if (totalCount == 0) { if (totalTrackCount == 0) {
return true; return true;
} }
for (int i = 0; i < totalCount; i++) { for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) { if (isCancelled()) {
// If cancelled, return true to show the number of files imported // If cancelled, return true to show the number of files imported
return true; return true;
} }
if (importFile(files.get(i))) { if (importFile(files.get(i))) {
successCount++; importTrackCount++;
} }
publishProgress(i + 1, totalCount); publishProgress(i + 1, totalTrackCount);
} }
return true; return true;
} finally { } finally {
@@ -146,7 +144,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected void onPostExecute(Boolean result) { protected void onPostExecute(Boolean result) {
completed = true; completed = true;
if (importActivity != null) { if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount); importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
} }
} }
@@ -154,7 +152,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected void onCancelled() { protected void onCancelled() {
completed = true; completed = true;
if (importActivity != null) { if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount); importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
} }
} }
@@ -190,9 +188,9 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
} }
/** /**
* Gets a list of files. If importAll is true, returns a list of the files * Gets a list of files.
* under the path directory. If importAll is false, returns a list containing * If importAll is true, returns a list of the files under the path directory.
* just the path file. * If importAll is false, returns a list containing just the path file.
*/ */
private List<File> getFiles() { private List<File> getFiles() {
List<File> files = new ArrayList<>(); List<File> files = new ArrayList<>();
@@ -22,16 +22,16 @@ import android.net.Uri;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import java.util.ArrayList; import java.util.ArrayList;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/** /**
* Imports a KML file. * Imports a KML file.
* *
@@ -82,8 +82,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
} }
@VisibleForTesting @VisibleForTesting
KmlFileTrackImporter( KmlFileTrackImporter(Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
super(context, importTrackId, contentProviderUtils); super(context, importTrackId, contentProviderUtils);
} }
@@ -93,11 +92,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
switch (tag) { switch (tag) {
case TAG_PLACEMARK: case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY: case TAG_PHOTO_OVERLAY:
/* // Note that a track is contained in a Placemark, calling onWaypointStart will clear various track variables like name, category, and description.
* Note that a track is contained in a Placemark, calling onWaypointStart
* will clear various track variables like name, category, and
* description.
*/
onWaypointStart(); onWaypointStart();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_GX_MULTI_TRACK:
@@ -118,13 +113,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
@Override @Override
public void endElement(String uri, String localName, String tag) throws SAXException { public void endElement(String uri, String localName, String tag) throws SAXException {
//TODO Check if order is relevant (uses localname and tag); and convert to switch statement
if (tag.equals(TAG_KML)) { if (tag.equals(TAG_KML)) {
onFileEnd(); onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) { } else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
/* // Note that a track is contained in a Placemark, calling onWaypointend is save since waypointType is not set for a track.
* Note that a track is contained in a Placemark, calling onWaypointend is
* save since waypointType is not set for a track.
*/
onWaypointEnd(); onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) { } else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd(); onWaypointLocationEnd();
@@ -187,19 +180,23 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
*/ */
private void onWaypointEnd() throws SAXException { private void onWaypointEnd() throws SAXException {
// Add a waypoint if the waypointType matches // Add a waypoint if the waypointType matches
WaypointType type = null; WaypointType type;
if (WAYPOINT_STYLE.equals(waypointType)) { switch (waypointType) {
type = WaypointType.WAYPOINT; case WAYPOINT_STYLE:
} else if (STATISTICS_STYLE.equals(waypointType)) { type = WaypointType.WAYPOINT;
type = WaypointType.STATISTICS; break;
} case STATISTICS_STYLE:
if (type == null) { type = WaypointType.STATISTICS;
return; break;
default:
return;
} }
if (photoUrl != null) { if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl); Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment()); photoUrl = getPhotoUrl(uri.getLastPathSegment());
} }
addWaypoint(type); addWaypoint(type);
} }
@@ -20,10 +20,6 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@@ -33,6 +29,11 @@ import java.io.InputStream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* Imports a KMZ file. * Imports a KMZ file.
* *
@@ -41,6 +42,7 @@ import java.util.zip.ZipInputStream;
public class KmzTrackImporter implements TrackImporter { public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName(); private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = 4096;
private final Context context; private final Context context;
@@ -50,22 +52,19 @@ public class KmzTrackImporter implements TrackImporter {
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
* @param importTrackId track id to import to. This should not be -1L so that * @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
* images in the kmz file can be imported.
*/ */
public KmzTrackImporter(Context context, long importTrackId) { KmzTrackImporter(Context context, long importTrackId) {
this.context = context; this.context = context;
this.importTrackId = importTrackId; this.importTrackId = importTrackId;
} }
@Override @Override
public long importFile(InputStream inputStream) { public long importFile(InputStream inputStream) {
ZipInputStream zipInputStream = null;
long trackId = importTrackId; long trackId = importTrackId;
try { try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry; ZipEntry zipEntry;
zipInputStream = new ZipInputStream(inputStream);
while ((zipEntry = zipInputStream.getNextEntry()) != null) { while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted"); Log.d(TAG, "Thread interrupted");
@@ -93,14 +92,6 @@ public class KmzTrackImporter implements TrackImporter {
Log.e(TAG, "Unable to import file", e); Log.e(TAG, "Unable to import file", e);
cleanImport(trackId); cleanImport(trackId);
return -1L; return -1L;
} finally {
if (zipInputStream != null) {
try {
zipInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close zip input stream", e);
}
}
} }
} }
@@ -110,7 +101,7 @@ public class KmzTrackImporter implements TrackImporter {
* @param trackId the trackId * @param trackId the trackId
*/ */
private void cleanImport(long trackId) { private void cleanImport(long trackId) {
if (trackId != -1L) { if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
} }
@@ -169,19 +160,17 @@ public class KmzTrackImporter implements TrackImporter {
* @param fileName the file name * @param fileName the file name
*/ */
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException { private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
FileOutputStream fileOutputStream = null; if (importTrackId == -1L) {
try { return;
if (importTrackId == -1L) { }
return; if (fileName.equals("")) {
} return;
if (fileName.equals("")) { }
return; File dir = FileUtils.getPhotoDir(importTrackId);
} FileUtils.ensureDirectoryExists(dir);
File dir = FileUtils.getPhotoDir(importTrackId); File file = new File(dir, fileName);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName); try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int count; int count;
while ((count = zipInputStream.read(buffer)) != -1) { while ((count = zipInputStream.read(buffer)) != -1) {
@@ -189,10 +178,6 @@ public class KmzTrackImporter implements TrackImporter {
} }
FileUtils.updateMediaScanner(context, Uri.fromFile(file)); FileUtils.updateMediaScanner(context, Uri.fromFile(file));
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} }
} }
} }
@@ -28,7 +28,7 @@ public interface TrackImporter {
* Import a file. * Import a file.
* *
* @param inputStream the file's input stream * @param inputStream the file's input stream
* @return the imported track id or -1L. * @return the imported track id or RECORDING_TRACK_ID_DEFAULT.
*/ */
long importFile(InputStream inputStream); long importFile(InputStream inputStream);
} }
@@ -17,8 +17,7 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
/** /**
* This is a simple location listener policy that will always dictate the same * This is a simple location listener policy that will always dictate the same polling interval.
* polling interval.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -41,7 +40,7 @@ public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
} }
@Override @Override
public int getMinDistance() { public int getMinDistance_m() {
return 0; return 0;
} }
@@ -17,53 +17,48 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
/** /**
* A {@link LocationListenerPolicy} that will change based on how long the user * A {@link LocationListenerPolicy} that will change based on how long the user has been stationary.
* has been stationary. This policy will dictate a policy based on a min, max * This policy will dictate a policy based on a min, max and idle time.
* and idle time. The policy will dictate an interval bounded by min and max, * The policy will dictate an interval bounded by min and max, and is half of the idle time.
* and is half of the idle time.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy { public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
private final long minInterval; private final long minInterval_ms;
private final long maxInterval; private final long maxInterval_ms;
private final int minDistance; private final int minDistance_m;
// The time the user has been idle at the current location, in milliseconds. // The time the user has been idle at the current location, in milliseconds.
private long idleTime; private long idleTime;
/** /**
* Creates a policy that will be bounded by the given minInterval and * Creates a policy that will be bounded by the given minInterval_ms and maxInterval_ms.
* maxInterval.
* *
* @param minInterval the smallest interval this policy will dictate, in * @param minInterval_ms the smallest interval this policy will dictate
* milliseconds * @param maxInterval_ms the largest interval this policy will dictate
* @param maxInterval the largest interval this policy will dictate, in * @param minDistance_m the minimum distance
* milliseconds
* @param minDistance the minimum distance in meters
*/ */
public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) { public AdaptiveLocationListenerPolicy(long minInterval_ms, long maxInterval_ms, int minDistance_m) {
this.minInterval = minInterval; this.minInterval_ms = minInterval_ms;
this.maxInterval = maxInterval; this.maxInterval_ms = maxInterval_ms;
this.minDistance = minDistance; this.minDistance_m = minDistance_m;
} }
/* /*
* Returns an interval half of the idle time, but bounded by minInteval and * Returns an interval half of the idle time, but bounded by minInterval_ms and maxInterval_ms.
* maxInterval.
*/ */
@Override @Override
public long getDesiredPollingInterval() { public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2; long desiredInterval_ms = idleTime / 2;
// Round to second to avoid setting the interval too often // Round to second to avoid setting the interval too often
desiredInterval = (desiredInterval / 1000) * 1000; desiredInterval_ms = (desiredInterval_ms / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval), minInterval); return Math.max(Math.min(maxInterval_ms, desiredInterval_ms), minInterval_ms);
} }
@Override @Override
public int getMinDistance() { public int getMinDistance_m() {
return minDistance; return minDistance_m;
} }
@Override @Override
@@ -30,12 +30,10 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.widgets.TrackWidgetProvider; import de.dennisguse.opentracks.widgets.TrackWidgetProvider;
/** /**
* A service to control starting and stopping of a recording. This service, * A service to control starting and stopping of a recording.
* through the AndroidManifest.xml, is configured to only allow components of * This service, through the AndroidManifest.xml, is configured to only allow components of the same application to invoke it.
* the same application to invoke it. Thus this service can be used my the * Thus this service can be used by the app widget, {@link TrackWidgetProvider}, but not by other applications.
* app widget, {@link TrackWidgetProvider}, but not by other applications. This * This application delegates starting and stopping a recording to {@link TrackRecordingService} using RPC calls.
* application delegates starting and stopping a recording to
* {@link TrackRecordingService} using RPC calls.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
@@ -60,9 +58,8 @@ public class ControlRecordingService extends IntentService implements ServiceCon
} }
@VisibleForTesting @VisibleForTesting
@Nullable
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(@Nullable Intent intent) {
return new LocalBinder(); return new LocalBinder();
} }
@@ -104,8 +101,7 @@ public class ControlRecordingService extends IntentService implements ServiceCon
} }
/** /**
* Notifies all threads that connection to {@link TrackRecordingService} is * Notifies all threads that connection to {@link TrackRecordingService} is available.
* available.
*/ */
private synchronized void notifyConnected() { private synchronized void notifyConnected() {
connected = true; connected = true;
@@ -120,7 +116,6 @@ public class ControlRecordingService extends IntentService implements ServiceCon
try { try {
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// can safely ignore // can safely ignore
} }
} }
@@ -133,7 +128,7 @@ public class ControlRecordingService extends IntentService implements ServiceCon
} }
@VisibleForTesting @VisibleForTesting
public class LocalBinder extends Binder { class LocalBinder extends Binder {
ControlRecordingService getService() { ControlRecordingService getService() {
return ControlRecordingService.this; return ControlRecordingService.this;
} }
@@ -17,9 +17,10 @@ package de.dennisguse.opentracks.services;
import android.location.Location; import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
/** /**
* App's service. * App's service.
@@ -97,6 +98,7 @@ public interface ITrackRecordingService {
* *
* @param location the track point to be inserted * @param location the track point to be inserted
*/ */
@VisibleForTesting
void insertTrackPoint(Location location); void insertTrackPoint(Location location);
/** /**
@@ -105,11 +107,4 @@ public interface ITrackRecordingService {
* @return SensorDataSet object. * @return SensorDataSet object.
*/ */
SensorDataSet getSensorData(); SensorDataSet getSensorData();
/**
* Gets the current sensor manager state.
* <p>
* return a Sensor.SensorState enum value.
*/
SensorState getSensorState();
} }
@@ -17,9 +17,7 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
/** /**
* This is an interface for classes that will manage the location listener * This is an interface for classes that will manage the location listener policy.
* policy. Different policy options are: {@link AbsoluteLocationListenerPolicy}
* and {@link AdaptiveLocationListenerPolicy}.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -35,13 +33,12 @@ public interface LocationListenerPolicy {
/** /**
* Returns the minimum distance between updates. * Returns the minimum distance between updates.
*/ */
int getMinDistance(); int getMinDistance_m();
/** /**
* Notifies the amount of time the user has been idle at his current location. * Notifies the amount of time the user has been idle at his current location.
* *
* @param idleTime the time that the user has been idle at his current * @param idleTime the time that the user has been idle at his current location
* location
*/ */
void updateIdleTime(long idleTime); void updateIdleTime(long idleTime);
} }
@@ -57,7 +57,6 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager; import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory; import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
@@ -947,7 +946,7 @@ public class TrackRecordingService extends Service {
} }
try { try {
long interval = locationListenerPolicy.getDesiredPollingInterval(); long interval = locationListenerPolicy.getDesiredPollingInterval();
locationManagerConnector.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance(), locationListener); locationManagerConnector.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance_m(), locationListener);
currentRecordingInterval = interval; currentRecordingInterval = interval;
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "Could not register location listener.", e); Log.e(TAG, "Could not register location listener.", e);
@@ -1116,15 +1115,6 @@ public class TrackRecordingService extends Service {
return trackRecordingService.remoteSensorManager.getSensorDataSet(); return trackRecordingService.remoteSensorManager.getSensorDataSet();
} }
@Override
public SensorState getSensorState() {
if (trackRecordingService.remoteSensorManager == null) {
Log.d(TAG, "remoteSensorManager is null.");
return SensorState.NONE;
}
return trackRecordingService.remoteSensorManager.getSensorState();
}
/** /**
* 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.
@@ -25,12 +25,13 @@ import android.os.IBinder.DeathRecipient;
import android.os.RemoteException; import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
/** /**
* Wrapper for the track recording service. This handles service * Wrapper for the track recording service.
* start/bind/unbind/stop. The service must be started before it can be bound. * This handles service start/bind/unbind/stop.
* The service must be started before it can be bound.
* Returns the service if it is started and bound. * Returns the service if it is started and bound.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
@@ -38,8 +39,10 @@ import de.dennisguse.opentracks.BuildConfig;
public class TrackRecordingServiceConnection { public class TrackRecordingServiceConnection {
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName(); private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
private final Context context; private final Context context;
private final Runnable callback; private final Runnable callback;
private ITrackRecordingService trackRecordingService; private ITrackRecordingService trackRecordingService;
private final DeathRecipient deathRecipient = new DeathRecipient() { private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override @Override
@@ -48,6 +51,7 @@ public class TrackRecordingServiceConnection {
setTrackRecordingService(null); setTrackRecordingService(null);
} }
}; };
private final ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceConnected(ComponentName className, IBinder service) {
@@ -130,8 +134,7 @@ public class TrackRecordingServiceConnection {
return; return;
} }
if (!startIfNeeded if (!startIfNeeded && !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it."); Log.d(TAG, "Service is not started. Not binding it.");
return; return;
} }
@@ -30,11 +30,11 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState; import de.dennisguse.opentracks.content.sensor.SensorState;
import java.util.UUID;
/** /**
* Manages connection to Bluetooth LE heart rate monitor. * Manages connection to Bluetooth LE heart rate monitor.
* TODO: shutdown connection. * TODO: shutdown connection.
@@ -42,16 +42,20 @@ import java.util.UUID;
public class BluetoothConnectionManager { public class BluetoothConnectionManager {
// Message types sent to handler // Message types sent to handler
public static final int MESSAGE_DEVICE_NAME = 1; static final int MESSAGE_DEVICE_NAME = 1;
public static final int MESSAGE_READ = 2; static final int MESSAGE_READ = 2;
public static final String KEY_DEVICE_NAME = "device_name";
static final String KEY_DEVICE_NAME = "device_name";
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL); private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL); private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL); private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final Context context; private final Context context;
private final Handler handler; private final Handler handler;
private SensorState sensorState; private SensorState sensorState;
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() { private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@@ -100,7 +104,7 @@ public class BluetoothConnectionManager {
* *
* @param handler a handler for sending messages back to the UI activity * @param handler a handler for sending messages back to the UI activity
*/ */
public BluetoothConnectionManager(Context context, Handler handler) { BluetoothConnectionManager(Context context, Handler handler) {
this.context = context; this.context = context;
this.handler = handler; this.handler = handler;
this.sensorState = SensorState.NONE; this.sensorState = SensorState.NONE;
@@ -109,7 +113,7 @@ public class BluetoothConnectionManager {
/** /**
* Gets the sensor state. * Gets the sensor state.
*/ */
public synchronized SensorState getSensorState() { synchronized SensorState getSensorState() {
return sensorState; return sensorState;
} }
@@ -2,12 +2,12 @@ package de.dennisguse.opentracks.services.sensors;
import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattCharacteristic;
public final class BluetoothLEUtils { final class BluetoothLEUtils {
private BluetoothLEUtils() { private BluetoothLEUtils() {
} }
public static int parseHeartRate(BluetoothGattCharacteristic characteristic) { static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/ //DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue(); byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1; int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
@@ -34,7 +34,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Bluetooth sensor manager. * Bluetooth sensor manager.
* <p>
* TODO: Handle a BluetoothGatt.STATE_DISCONNECTED * TODO: Handle a BluetoothGatt.STATE_DISCONNECTED
* *
* @author Sandor Dornbush * @author Sandor Dornbush
@@ -42,10 +41,15 @@ import de.dennisguse.opentracks.util.UnitConversions;
public class BluetoothRemoteSensorManager extends RemoteSensorManager { public class BluetoothRemoteSensorManager extends RemoteSensorManager {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter(); private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
private final Context context; private final Context context;
private final BluetoothConnectionManager bluetoothConnectionManager; private final BluetoothConnectionManager bluetoothConnectionManager;
private SensorDataSet sensorDataSet = null; private SensorDataSet sensorDataSet = null;
// Handler that gets information back from the bluetoothConnectionManager // Handler that gets information back from the bluetoothConnectionManager
private final Handler messageHandler = new Handler(Looper.getMainLooper()) { private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override @Override
@@ -71,7 +75,7 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
/** /**
* @param context the context * @param context the context
*/ */
public BluetoothRemoteSensorManager(Context context) { BluetoothRemoteSensorManager(Context context) {
this.context = context; this.context = context;
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler); bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler);
} }
@@ -109,7 +113,6 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
if (adapters.get(0) == null) { if (adapters.get(0) == null) {
Log.w(TAG, "No bluetooth adapter found."); Log.w(TAG, "No bluetooth adapter found.");
return null;
} }
return adapters.get(0); return adapters.get(0);
} }
@@ -125,6 +128,7 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
Log.w(TAG, "Bluetooth not enabled."); Log.w(TAG, "Bluetooth not enabled.");
return; return;
} }
String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) { if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
Log.w(TAG, "No bluetooth address."); Log.w(TAG, "No bluetooth address.");
@@ -78,13 +78,10 @@ public abstract class RemoteSensorManager {
setUpChannel(); setUpChannel();
} }
break; break;
case NONE:
case DISCONNECTED: case DISCONNECTED:
setUpChannel(); setUpChannel();
break; break;
default: default: // NONE, CONNECTED or SENDING
// CONNECTED or SENDING
break;
} }
} }
}; };
@@ -26,6 +26,8 @@ import android.content.Context;
public class RemoteSensorManagerFactory { public class RemoteSensorManagerFactory {
private static RemoteSensorManager remoteSensorManagerSystem = null; private static RemoteSensorManager remoteSensorManagerSystem = null;
//TODO Check if still needed? Is there a missing features?
private static RemoteSensorManager remoteSensorManagerTemporary = null; private static RemoteSensorManager remoteSensorManagerTemporary = null;
private RemoteSensorManagerFactory() { private RemoteSensorManagerFactory() {
@@ -70,7 +72,7 @@ public class RemoteSensorManagerFactory {
/** /**
* Releases the temp sensor manager. * Releases the temp sensor manager.
*/ */
public static void releaseSensorManagerTemporary() { private static void releaseSensorManagerTemporary() {
if (remoteSensorManagerTemporary != null) { if (remoteSensorManagerTemporary != null) {
remoteSensorManagerTemporary.stopSensor(); remoteSensorManagerTemporary.stopSensor();
} }
@@ -27,14 +27,14 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
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.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.Locale;
/** /**
* This class will periodically announce the user's trip statistics. * This class will periodically announce the user's trip statistics.
@@ -50,7 +50,9 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
static final float TTS_SPEECH_RATE = 0.9f; static final float TTS_SPEECH_RATE = 0.9f;
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName(); private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
private final Context context; private final Context context;
private final AudioManager audioManager; private final AudioManager audioManager;
private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() { private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() {
@Override @Override
@@ -69,13 +71,16 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
public void onError(String utteranceId) { public void onError(String utteranceId) {
} }
}; };
private TextToSpeech tts; private TextToSpeech tts;
// Response from TTS after its initialization // Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR; private int initStatus = TextToSpeech.ERROR;
// True if TTS engine is ready
private boolean ready = false; private boolean ttsReady = false;
// True if speech is allowed // True if speech is allowed
private boolean speechAllowed; private boolean speechAllowed;
/** /**
* Listener which updates {@link #speechAllowed} when the phone state changes. * Listener which updates {@link #speechAllowed} when the phone state changes.
*/ */
@@ -90,7 +95,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
} }
}; };
public AnnouncementPeriodicTask(Context context) { AnnouncementPeriodicTask(Context context) {
this.context = context; this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
} }
@@ -131,13 +136,13 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
} }
synchronized (this) { synchronized (this) {
if (!ready) { if (!ttsReady) {
ready = initStatus == TextToSpeech.SUCCESS; ttsReady = initStatus == TextToSpeech.SUCCESS;
if (ready) { if (ttsReady) {
onTtsReady(); onTtsReady();
} }
} }
if (!ready) { if (!ttsReady) {
Log.i(TAG, "TTS not ready."); Log.i(TAG, "TTS not ready.");
return; return;
} }
@@ -170,9 +175,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.w(TAG, "Default locale not available, use English."); Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH; locale = Locale.ENGLISH;
/* /*
* TODO: instead of using english, load the language if missing and show a * TODO: instead of using english, load the language if missing and show a toast if not supported.
* toast if not supported. Not able to change the resource strings to * Not able to change the resource strings to English.
* English.
*/ */
} }
tts.setLanguage(locale); tts.setLanguage(locale);
@@ -189,8 +193,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
* @param announcement the announcement * @param announcement the announcement
*/ */
private void speakAnnouncement(String announcement) { private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus( int result = audioManager.requestAudioFocus(null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus."); Log.w(TAG, "Failed to request audio focus.");
} }
@@ -223,7 +226,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context); boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM; double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH; double distancePerTime = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (distance == 0) { if (distance == 0) {
return context.getString(R.string.voice_total_distance_zero); return context.getString(R.string.voice_total_distance_zero);
@@ -231,26 +234,22 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
if (!metricUnits) { if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
speed *= UnitConversions.KM_TO_MI; distancePerTime *= UnitConversions.KM_TO_MI;
} }
String rate; String rate;
if (reportSpeed) { if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
: R.plurals.voiceSpeedMilesPerHour; rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
} else { } else {
speed = speed == 0 ? 0.0 : 1 / speed; double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
long time = Math.round( long time = Math.round(timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
speed * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
rate = context.getString(paceId, getAnnounceTime(time)); rate = context.getString(paceId, getAnnounceTime(time));
} }
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
: R.plurals.voiceTotalDistanceMiles; String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
String totalDistance = context.getResources()
.getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
return context.getString(R.string.voice_template, totalDistance, return context.getString(R.string.voice_template, totalDistance,
getAnnounceTime(tripStatistics.getMovingTime()), rate); getAnnounceTime(tripStatistics.getMovingTime()), rate);
@@ -264,8 +263,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
*/ */
@VisibleForTesting @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) { protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony = (TelephonyManager) context.getSystemService( TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
Context.TELEPHONY_SERVICE);
if (telephony != null) { if (telephony != null) {
telephony.listen(listener, events); telephony.listen(listener, events);
} }
@@ -297,15 +295,13 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
} }
/** /**
* Gets the plural count to be used by getQuantityString. getQuantityString * Gets the plural count to be used by getQuantityString.
* only supports integer quantities, not a double quantity like "2.2". * getQuantityString only supports integer quantities, not a double quantity like "2.2".
* <p> * <p>
* As a temporary workaround, we convert a double quantity to an integer * As a temporary workaround, we convert a double quantity to an integer quantity.
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return * If the double quantity is exactly 0, 1, or 2, then we can return these integer quantities.
* these integer quantities. Otherwise, we cast the double quantity to an * Otherwise, we cast the double quantity to an integer quantity.
* integer quantity. However, we need to make sure that if the casted value is * However, we need to make sure that if the casted value is 0, 1, or 2, we don't return those, instead, return the next biggest integer 3.
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
* 3.
* *
* @param d the double value * @param d the double value
*/ */
@@ -317,6 +313,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
} else if (d == 2) { } else if (d == 2) {
return 2; return 2;
} else { } else {
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
int count = (int) d; int count = (int) d;
return count < 3 ? 3 : count; return count < 3 ? 3 : count;
} }
@@ -24,6 +24,7 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public interface PeriodicTask { public interface PeriodicTask {
/** /**
* Sets up this task for subsequent calls to the run method. * Sets up this task for subsequent calls to the run method.
*/ */
@@ -36,9 +36,10 @@ public class PeriodicTaskExecutor {
private final PeriodicTaskFactory periodicTaskFactory; private final PeriodicTaskFactory periodicTaskFactory;
/** /**
* The task frequency. A positive value is a time frequency (minutes). A * The task frequency.
* negative value is a distance frequency (km or mi). A zero value is to turn * A positive value is a time frequency (minutes).
* off periodic task. * A negative value is a distance frequency (km or mi).
* A zero value is to turn off periodic task.
*/ */
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF; private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
@@ -52,8 +53,7 @@ public class PeriodicTaskExecutor {
// The next distance for the distance periodic task // The next distance for the distance periodic task
private double nextTaskDistance = Double.MAX_VALUE; private double nextTaskDistance = Double.MAX_VALUE;
public PeriodicTaskExecutor( public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory; this.periodicTaskFactory = periodicTaskFactory;
} }
@@ -67,11 +67,9 @@ public class PeriodicTaskExecutor {
return; return;
} }
if (!isTimeFrequency()) { if (!isTimeFrequency() && timerTaskExecutor != null) {
if (timerTaskExecutor != null) { timerTaskExecutor.shutdown();
timerTaskExecutor.shutdown(); timerTaskExecutor = null;
timerTaskExecutor = null;
}
} }
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) { if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off."); Log.d(TAG, "Task frequency is off.");
@@ -119,12 +117,13 @@ public class PeriodicTaskExecutor {
if (!isDistanceFrequency() || periodicTask == null) { if (!isDistanceFrequency() || periodicTask == null) {
return; return;
} }
TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) { if (tripStatistics == null) {
return; return;
} }
double distance = tripStatistics.getTotalDistance()
* UnitConversions.M_TO_KM; double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) { if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
} }
@@ -159,8 +158,7 @@ public class PeriodicTaskExecutor {
* Calculates the next distance for the distance periodic task. * Calculates the next distance for the distance periodic task.
*/ */
private void calculateNextTaskDistance() { private void calculateNextTaskDistance() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
|| periodicTask == null) {
return; return;
} }
@@ -175,11 +173,11 @@ public class PeriodicTaskExecutor {
return; return;
} }
double distance = tripStatistics.getTotalDistance() double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
* UnitConversions.M_TO_KM;
if (!metricUnits) { if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
} }
// The index will be negative since the frequency is negative. // The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency); int index = (int) (distance / taskFrequency);
index -= 1; index -= 1;
@@ -16,19 +16,19 @@
package de.dennisguse.opentracks.services.tasks; package de.dennisguse.opentracks.services.tasks;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import java.util.Date; import java.util.Date;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* This class will periodically perform a task. * This class will periodically perform a task.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class TimerTaskExecutor { class TimerTaskExecutor {
private final PeriodicTask periodicTask; private final PeriodicTask periodicTask;
private final TrackRecordingService trackRecordingService; private final TrackRecordingService trackRecordingService;
@@ -42,12 +42,12 @@ public class TimerTaskExecutor {
} }
/** /**
* Schedules the periodic task at an interval. * Schedules the periodic task in milliseconds.
* *
* @param interval the interval in milliseconds * @param interval_ms the interval_ms
*/ */
public void scheduleTask(long interval) { void scheduleTask(long interval_ms) {
if (interval <= 0) { if (interval_ms <= 0) {
return; return;
} }
@@ -69,14 +69,11 @@ public class TimerTaskExecutor {
} }
}; };
timer = new Timer(TimerTaskExecutor.class.getSimpleName()); timer = new Timer(TimerTaskExecutor.class.getSimpleName());
long next = System.currentTimeMillis() + interval - (tripStatistics.getTotalTime() % interval); long next = System.currentTimeMillis() + interval_ms - (tripStatistics.getTotalTime() % interval_ms);
timer.scheduleAtFixedRate(timerTask, new Date(next), interval); timer.scheduleAtFixedRate(timerTask, new Date(next), interval_ms);
} }
/** void shutdown() {
* Shuts down.
*/
public void shutdown() {
if (timerTask != null) { if (timerTask != null) {
timerTask.cancel(); timerTask.cancel();
timerTask = null; timerTask = null;
@@ -77,8 +77,7 @@ public class ActivityTypePreference extends DialogPreference {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) { if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue( String iconValue = TrackIconUtils.getIconValue(getContext(), textView.getText().toString());
getContext(), textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue); TrackIconUtils.setIconSpinner(spinner, iconValue);
} }
} }
@@ -136,7 +135,7 @@ public class ActivityTypePreference extends DialogPreference {
* *
* @param activity the activity * @param activity the activity
*/ */
public void setRecordingSettingsActivity(RecordingSettingsActivity activity) { void setRecordingSettingsActivity(RecordingSettingsActivity activity) {
recordingSettingsActivity = activity; recordingSettingsActivity = activity;
} }
@@ -145,10 +144,9 @@ public class ActivityTypePreference extends DialogPreference {
* *
* @param iconValue the icon value * @param iconValue the icon value
*/ */
public void updateValue(String iconValue) { void updateValue(String iconValue) {
TrackIconUtils.setIconSpinner(spinner, iconValue); TrackIconUtils.setIconSpinner(spinner, iconValue);
textView.setText( textView.setText(recordingSettingsActivity.getString(TrackIconUtils.getIconActivityType(iconValue)));
recordingSettingsActivity.getString(TrackIconUtils.getIconActivityType(iconValue)));
textView.clearFocus(); textView.clearFocus();
} }
} }
@@ -22,20 +22,19 @@ import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
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.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
/** /**
* An activity for accessing recording settings. * An activity for accessing recording settings.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public class RecordingSettingsActivity extends PreferenceActivity public class RecordingSettingsActivity extends PreferenceActivity implements ChooseActivityTypeCaller {
implements ChooseActivityTypeCaller {
private static final int DIALOG_CHOOSE_ACTIVITY = 0; private static final int DIALOG_CHOOSE_ACTIVITY = 0;
@@ -51,13 +50,10 @@ public class RecordingSettingsActivity extends PreferenceActivity
configFrequencyPreference(R.string.voice_frequency_key, metricUnits); configFrequencyPreference(R.string.voice_frequency_key, metricUnits);
configFrequencyPreference(R.string.split_frequency_key, metricUnits); configFrequencyPreference(R.string.split_frequency_key, metricUnits);
configDefaultActivity(); configDefaultActivity();
configListPreference(R.string.min_recording_interval_key, R.array.min_recording_interval_values, configListPreference(R.string.min_recording_interval_key, R.array.min_recording_interval_values, metricUnits);
metricUnits);
configListPreference(R.string.recording_distance_interval_key, R.array.recording_distance_interval_values, metricUnits); configListPreference(R.string.recording_distance_interval_key, R.array.recording_distance_interval_values, metricUnits);
configListPreference(R.string.max_recording_distance_key, R.array.max_recording_distance_values, configListPreference(R.string.max_recording_distance_key, R.array.max_recording_distance_values, metricUnits);
metricUnits); configListPreference(R.string.recording_gps_accuracy_key, R.array.recording_gps_accuracy_values, metricUnits);
configListPreference(R.string.recording_gps_accuracy_key, R.array.recording_gps_accuracy_values,
metricUnits);
configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits); configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits);
} }
@@ -80,31 +76,25 @@ public class RecordingSettingsActivity extends PreferenceActivity
} }
private void configDefaultActivity() { private void configDefaultActivity() {
activityTypePreference = (ActivityTypePreference) findPreference( activityTypePreference = (ActivityTypePreference) findPreference(getString(R.string.default_activity_key));
getString(R.string.default_activity_key));
String defaultActivity = PreferencesUtils.getString( String defaultActivity = PreferencesUtils.getString(
this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
activityTypePreference.setOnPreferenceChangeListener( activityTypePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
new Preference.OnPreferenceChangeListener() { @Override
@Override public boolean onPreferenceChange(Preference pref, Object newValue) {
public boolean onPreferenceChange(Preference pref, Object newValue) { String stringValue = (String) newValue;
String stringValue = (String) newValue; pref.setSummary(stringValue != null && !stringValue.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? stringValue : getString(R.string.value_unknown));
pref.setSummary(stringValue != null return true;
&& !stringValue.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? stringValue }
: getString(R.string.value_unknown)); });
return true; activityTypePreference.setSummary(defaultActivity != null && !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity : getString(R.string.value_unknown));
}
});
activityTypePreference.setSummary(defaultActivity != null
&& !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity
: getString(R.string.value_unknown));
activityTypePreference.setRecordingSettingsActivity(this); activityTypePreference.setRecordingSettingsActivity(this);
} }
private void configListPreference(int key, int valueArray, boolean metricUnits) { private void configListPreference(int key, int valueArray, boolean metricUnits) {
ListPreference preference = (ListPreference) findPreference(getString(key)); ListPreference preference = (ListPreference) findPreference(getString(key));
@Deprecated @Deprecated //TODO Can we make values an int array?
String[] values = getResources().getStringArray(valueArray); String[] values = getResources().getStringArray(valueArray);
String[] options = new String[values.length]; String[] options = new String[values.length];
switch (key) { switch (key) {
@@ -59,10 +59,7 @@ public class StatsSettingsActivity extends PreferenceActivity {
listPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { listPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override @Override
public boolean onPreferenceChange(Preference pref, Object newValue) { public boolean onPreferenceChange(Preference pref, Object newValue) {
if (listener != null) { return listener.onPreferenceChange(pref, newValue);
listener.onPreferenceChange(pref, newValue);
}
return true;
} }
}); });
} }
@@ -19,9 +19,9 @@ package de.dennisguse.opentracks.stats;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
/** /**
* This class maintains a buffer of doubles. This buffer is a convenient class * This class maintains a buffer of doubles.
* for storing a series of doubles and calculating information about them. This * This buffer is a convenient class for storing a series of doubles and calculating information about them.
* is a FIFO buffer. * This is a FIFO buffer.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -29,8 +29,10 @@ public class DoubleBuffer {
// The sliding buffer of doubles. // The sliding buffer of doubles.
private final double[] buffer; private final double[] buffer;
// The location that the next write will occur at. // The location that the next write will occur at.
private int index; private int index;
// True if the buffer is full // True if the buffer is full
private boolean isFull; private boolean isFull;
@@ -39,7 +41,7 @@ public class DoubleBuffer {
* *
* @param size the size * @param size the size
*/ */
public DoubleBuffer(int size) { DoubleBuffer(int size) {
if (size < 1) { if (size < 1) {
throw new IllegalArgumentException("The buffer size must be greater than 1."); throw new IllegalArgumentException("The buffer size must be greater than 1.");
} }
@@ -58,7 +60,7 @@ public class DoubleBuffer {
/** /**
* Returns true if the buffer is full. * Returns true if the buffer is full.
*/ */
public boolean isFull() { boolean isFull() {
return isFull; return isFull;
} }
@@ -78,8 +80,8 @@ public class DoubleBuffer {
} }
/** /**
* Adds a double to the buffer. If the buffer is full the oldest element is * Adds a double to the buffer.
* overwritten. * If the buffer is full the oldest element is overwritten.
* *
* @param value the double to add * @param value the double to add
*/ */
@@ -35,6 +35,7 @@ public class TripStatistics implements Parcelable {
* Creator of {@link TripStatistics} from parcels. * Creator of {@link TripStatistics} from parcels.
*/ */
public static final Creator CREATOR = new Creator(); public static final Creator CREATOR = new Creator();
// The min and max latitude seen in this trip. // The min and max latitude seen in this trip.
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
// The min and max longitude seen in this trip. // The min and max longitude seen in this trip.
@@ -43,6 +44,7 @@ public class TripStatistics implements Parcelable {
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
// The min and max grade seen on this trip. // The min and max grade seen on this trip.
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
// The trip start time. This is the system time, might not match the GPs time. // The trip start time. This is the system time, might not match the GPs time.
private long startTime = -1L; private long startTime = -1L;
// The trip stop time. This is the system time, might not match the GPS time. // The trip stop time. This is the system time, might not match the GPS time.
@@ -173,10 +175,9 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the total time in milliseconds that this track has been active. This * Gets the total time in milliseconds that this track has been active.
* statistic is only updated when a new point is added to the statistics, so * This statistic is only updated when a new point is added to the statistics, so it may be off.
* it may be off. If you need to calculate the proper total time, use * If you need to calculate the proper total time, use {@link #getStartTime} with the current time.
* {@link #getStartTime} with the current time.
*/ */
public long getTotalTime() { public long getTotalTime() {
return totalTime; return totalTime;
@@ -217,16 +218,14 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the topmost position (highest latitude) of the track, in signed * Gets the topmost position (highest latitude) of the track, in signed degrees.
* degrees.
*/ */
public double getTopDegrees() { public double getTopDegrees() {
return latitudeExtremities.getMax(); return latitudeExtremities.getMax();
} }
/** /**
* Gets the topmost position (highest latitude) of the track, in signed * Gets the topmost position (highest latitude) of the track, in signed millions of degrees.
* millions of degrees.
*/ */
public int getTop() { public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6); return (int) (latitudeExtremities.getMax() * 1E6);
@@ -241,40 +240,35 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the bottommost position (lowest latitude) of the track, in signed * Gets the bottommost position (lowest latitude) of the track, in signed millions of degrees.
* millions of degrees.
*/ */
public int getBottom() { public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6); return (int) (latitudeExtremities.getMin() * 1E6);
} }
/** /**
* Gets the leftmost position (lowest longitude) of the track, in signed * Gets the leftmost position (lowest longitude) of the track, in signed degrees.
* degrees.
*/ */
public double getLeftDegrees() { public double getLeftDegrees() {
return longitudeExtremities.getMin(); return longitudeExtremities.getMin();
} }
/** /**
* Gets the leftmost position (lowest longitude) of the track, in signed * Gets the leftmost position (lowest longitude) of the track, in signed millions of degrees.
* millions of degrees.
*/ */
public int getLeft() { public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6); return (int) (longitudeExtremities.getMin() * 1E6);
} }
/** /**
* Gets the rightmost position (highest longitude) of the track, in signed * Gets the rightmost position (highest longitude) of the track, in signed degrees.
* degrees.
*/ */
public double getRightDegrees() { public double getRightDegrees() {
return longitudeExtremities.getMax(); return longitudeExtremities.getMax();
} }
/** /**
* Gets the rightmost position (highest longitude) of the track, in signed * Gets the rightmost position (highest longitude) of the track, in signed millions of degrees.
* millions of degrees.
*/ */
public int getRight() { public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6); return (int) (longitudeExtremities.getMax() * 1E6);
@@ -327,9 +321,8 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the average speed in meters/second. This calculation only takes into * Gets the average speed in meters/second.
* account the displacement until the last point that was accounted for in * This calculation only takes into account the displacement until the last point that was accounted for in statistics.
* statistics.
*/ */
public double getAverageSpeed() { public double getAverageSpeed() {
if (totalTime == 0L) { if (totalTime == 0L) {
@@ -365,8 +358,8 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the minimum elevation. This is calculated from the smoothed elevation * Gets the minimum elevation.
* so this can actually be more than the current elevation. * This is calculated from the smoothed elevation, so this can actually be more than the current elevation.
*/ */
public double getMinElevation() { public double getMinElevation() {
return elevationExtremities.getMin(); return elevationExtremities.getMin();
@@ -382,8 +375,8 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the maximum elevation. This is calculated from the smoothed elevation * Gets the maximum elevation.
* so this can actually be less than the current elevation. * This is calculated from the smoothed elevation, so this can actually be less than the current elevation.
*/ */
public double getMaxElevation() { public double getMaxElevation() {
return elevationExtremities.getMax(); return elevationExtremities.getMax();
@@ -408,8 +401,7 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the total elevation gain in meters. This is calculated as the sum of * Gets the total elevation gain in meters. This is calculated as the sum of all positive differences in the smoothed elevation.
* all positive differences in the smoothed elevation.
*/ */
public double getTotalElevationGain() { public double getTotalElevationGain() {
return totalElevationGain; return totalElevationGain;
@@ -27,9 +27,9 @@ import static de.dennisguse.opentracks.services.TrackRecordingService.MAX_NO_MOV
import static de.dennisguse.opentracks.services.TrackRecordingService.PAUSE_LATITUDE; import static de.dennisguse.opentracks.services.TrackRecordingService.PAUSE_LATITUDE;
/** /**
* Updater for {@link TripStatistics}. For updating track trip statistics as new * Updater for {@link TripStatistics}.
* locations are added. Note that some of the locations represent pause/resume * For updating track trip statistics as new locations are added.
* separator. * NOTE:Some of the locations represent pause/resume separator.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
@@ -45,28 +45,30 @@ public class TripStatisticsUpdater {
* signal. * signal.
*/ */
@VisibleForTesting @VisibleForTesting
static final int ELEVATION_SMOOTHING_FACTOR = 25; private static final int ELEVATION_SMOOTHING_FACTOR = 25;
/** /**
* The number of run readings to smooth for calculating grade. * The number of run readings to smooth for calculating grade.
*/ */
@VisibleForTesting @VisibleForTesting
static final int RUN_SMOOTHING_FACTOR = 25; private static final int RUN_SMOOTHING_FACTOR = 25;
/** /**
* The number of speed reading to smooth to get a somewhat accurate signal. * The number of speed reading to smooth to get a somewhat accurate signal.
*/ */
@VisibleForTesting @VisibleForTesting
static final int SPEED_SMOOTHING_FACTOR = 25; private static final int SPEED_SMOOTHING_FACTOR = 25;
private static final String TAG = TripStatisticsUpdater.class.getSimpleName(); private static final String TAG = TripStatisticsUpdater.class.getSimpleName();
/** /**
* Ignore any acceleration faster than this. Will ignore any speeds that imply * Ignore any acceleration faster than this.
* acceleration greater than 2g's 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 * Will ignore any speeds that imply acceleration greater than 2g's
* m/(m*ms) * 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
*/ */
private static final double MAX_ACCELERATION = 0.02; private static final double MAX_ACCELERATION = 0.02;
// The track's trip statistics // The track's trip statistics
private final TripStatistics tripStatistics; private final TripStatistics tripStatistics;
// A buffer of the recent elevation readings (m) // A buffer of the recent elevation readings (m)
private final DoubleBuffer elevationBuffer = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR); private final DoubleBuffer elevationBuffer = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR);
// A buffer of the recent run readings (m) for calculating grade // A buffer of the recent run readings (m) for calculating grade
@@ -75,6 +77,7 @@ public class TripStatisticsUpdater {
private final DoubleBuffer gradeBuffer = new DoubleBuffer(GRADE_SMOOTHING_FACTOR); private final DoubleBuffer gradeBuffer = new DoubleBuffer(GRADE_SMOOTHING_FACTOR);
// A buffer of the recent speed readings (m/s) for calculating max speed // A buffer of the recent speed readings (m/s) for calculating max speed
private final DoubleBuffer speedBuffer = new DoubleBuffer(SPEED_SMOOTHING_FACTOR); private final DoubleBuffer speedBuffer = new DoubleBuffer(SPEED_SMOOTHING_FACTOR);
// The current segment's trip statistics // The current segment's trip statistics
private TripStatistics currentSegment; private TripStatistics currentSegment;
// Current segment's last location. // Current segment's last location.
@@ -108,7 +111,8 @@ public class TripStatisticsUpdater {
} }
/** /**
* Adds a location. TODO: This assume location has a valid time. * Adds a location.
* TODO: This assume location has a valid time.
* *
* @param location the location * @param location the location
* @param minRecordingDistance the min recording distance * @param minRecordingDistance the min recording distance
@@ -119,8 +123,7 @@ public class TripStatisticsUpdater {
if (!LocationUtils.isValidLocation(location)) { if (!LocationUtils.isValidLocation(location)) {
// Either pause or resume marker // Either pause or resume marker
if (location.getLatitude() == PAUSE_LATITUDE) { if (location.getLatitude() == PAUSE_LATITUDE) {
if (lastLocation != null && lastMovingLocation != null if (lastLocation != null && lastMovingLocation != null && lastLocation != lastMovingLocation) {
&& lastLocation != lastMovingLocation) {
currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation)); currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation));
} }
tripStatistics.merge(currentSegment); tripStatistics.merge(currentSegment);
@@ -134,11 +137,12 @@ public class TripStatisticsUpdater {
speedBuffer.reset(); speedBuffer.reset();
return; return;
} }
currentSegment.updateLatitudeExtremities(location.getLatitude()); currentSegment.updateLatitudeExtremities(location.getLatitude());
currentSegment.updateLongitudeExtremities(location.getLongitude()); currentSegment.updateLongitudeExtremities(location.getLongitude());
double elevationDifference = location.hasAltitude() ? updateElevation(location.getAltitude()) //TODO Use Barometer to compute elevation gain.
: 0.0; double elevationDifference = location.hasAltitude() ? updateElevation(location.getAltitude()) : 0.0;
if (lastLocation == null || lastMovingLocation == null) { if (lastLocation == null || lastMovingLocation == null) {
lastLocation = location; lastLocation = location;
@@ -147,8 +151,7 @@ public class TripStatisticsUpdater {
} }
double movingDistance = lastMovingLocation.distanceTo(location); double movingDistance = lastMovingLocation.distanceTo(location);
if (movingDistance < minRecordingDistance if (movingDistance < minRecordingDistance && (!location.hasSpeed() || location.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
&& (!location.hasSpeed() || location.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
speedBuffer.reset(); speedBuffer.reset();
lastLocation = location; lastLocation = location;
return; return;
@@ -171,8 +174,7 @@ public class TripStatisticsUpdater {
// Update max speed // Update max speed
if (location.hasSpeed() && lastLocation.hasSpeed()) { if (location.hasSpeed() && lastLocation.hasSpeed()) {
updateSpeed( updateSpeed(location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
} }
lastLocation = location; lastLocation = location;
@@ -180,9 +182,8 @@ public class TripStatisticsUpdater {
} }
/** /**
* Gets the smoothed elevation over several readings. The elevation readings * Gets the smoothed elevation over several readings.
* is noisy so the smoothed elevation is better than the raw elevation for * The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks.
* many tasks.
*/ */
public double getSmoothedElevation() { public double getSmoothedElevation() {
return elevationBuffer.getAverage(); return elevationBuffer.getAverage();
@@ -193,7 +194,7 @@ public class TripStatisticsUpdater {
} }
/** /**
* Updates a speed reading. Assumes the user is moving. * Updates a speed reading while assuming the user is moving.
* *
* @param time the time * @param time the time
* @param speed the speed * @param speed the speed
@@ -201,7 +202,7 @@ public class TripStatisticsUpdater {
* @param lastLocationSpeed the last location speed * @param lastLocationSpeed the last location speed
*/ */
@VisibleForTesting @VisibleForTesting
void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { private void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
if (speed < MAX_NO_MOVEMENT_SPEED) { if (speed < MAX_NO_MOVEMENT_SPEED) {
speedBuffer.reset(); speedBuffer.reset();
} else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) { } else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) {
@@ -220,7 +221,7 @@ public class TripStatisticsUpdater {
* @param elevation the elevation * @param elevation the elevation
*/ */
@VisibleForTesting @VisibleForTesting
double updateElevation(double elevation) { private double updateElevation(double elevation) {
// Update elevation using the smoothed average // Update elevation using the smoothed average
double oldAverage = elevationBuffer.getAverage(); double oldAverage = elevationBuffer.getAverage();
elevationBuffer.setNext(elevation); elevationBuffer.setNext(elevation);
@@ -241,15 +242,12 @@ public class TripStatisticsUpdater {
* @param rise the rise * @param rise the rise
*/ */
@VisibleForTesting @VisibleForTesting
void updateGrade(double run, double rise) { private void updateGrade(double run, double rise) {
runBuffer.setNext(run); runBuffer.setNext(run);
double smoothedRun = runBuffer.getAverage(); double smoothedRun = runBuffer.getAverage();
/* // With the error in the altitude measurement, it is dangerous to divide by * anything less than 5.
* With the error in the altitude measurement it is dangerous to divide by
* anything less than 5.
*/
if (smoothedRun < 5.0) { if (smoothedRun < 5.0) {
return; return;
} }
@@ -272,46 +270,31 @@ public class TripStatisticsUpdater {
* @param lastLocationTime the last location time * @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed * @param lastLocationSpeed the last location speed
*/ */
private boolean isValidSpeed( private boolean isValidSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
long time, double speed, long lastLocationTime, double lastLocationSpeed) { // 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 (speed == 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(speed - 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 = time - lastLocationTime;
double speedDifference = Math.abs(lastLocationSpeed - speed); double speedDifference = Math.abs(lastLocationSpeed - speed);
if (speedDifference > MAX_ACCELERATION * timeDifference) { if (speedDifference > MAX_ACCELERATION * timeDifference) {
return false; return false;
} }
/* // 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.isFull()) { if (speedBuffer.isFull()) {
double average = speedBuffer.getAverage(); double average = speedBuffer.getAverage();
double diff = Math.abs(average - speed); double diff = Math.abs(average - speed);
return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference); return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
} else {
return true;
} }
return true;
} }
} }
@@ -34,13 +34,13 @@ public class FileUtils {
/** /**
* Used to transfer picture from the camera. * Used to transfer picture from the camera.
*/ */
public static final String FILEPROVIDER = BuildConfig.APPLICATION_ID + ".fileprovider"; static final String FILEPROVIDER = BuildConfig.APPLICATION_ID + ".fileprovider";
/** /**
* Our external storage directory. * Our external storage directory.
*/ */
static final String SDCARD_TOP_DIR = "OpenTracks"; static final String SDCARD_TOP_DIR = "OpenTracks";
static final String TRACK_PREFIX = "OpenTracks"; private static final String TRACK_PREFIX = "OpenTracks";
/** /**
* The maximum FAT32 path length. See the FAT32 spec at * The maximum FAT32 path length. See the FAT32 spec at
* http://msdn.microsoft.com/en-us/windows/hardware/gg463080 * http://msdn.microsoft.com/en-us/windows/hardware/gg463080
@@ -127,16 +127,14 @@ public class FileUtils {
} }
/** /**
* Builds a filename with the given base name (prefix) and the given * Builds a filename with the given base name (prefix) and the given extension, possibly adding a suffix to ensure the file doesn't exist.
* extension, possibly adding a suffix to ensure the file doesn't exist.
* *
* @param directory the directory the file will live in * @param directory the directory the file will live in
* @param fileBaseName the prefix for the file name * @param fileBaseName the prefix for the file name
* @param extension the file's extension * @param extension the file's extension
* @return the complete file name, without the directory * @return the complete file name, without the directory
*/ */
public static synchronized String buildUniqueFileName( public static synchronized String buildUniqueFileName(File directory, String fileBaseName, String extension) {
File directory, String fileBaseName, String extension) {
return buildUniqueFileName(directory, fileBaseName, extension, 0); return buildUniqueFileName(directory, fileBaseName, extension, 0);
} }
@@ -154,7 +152,8 @@ public class FileUtils {
} }
/** /**
* Gets the extension from a file name. Returns null if there is no extension. * Gets the extension from a file name.
* Returns null if there is no extension.
* *
* @param fileName the file name * @param fileName the file name
*/ */
@@ -173,8 +172,7 @@ public class FileUtils {
} }
/** /**
* Builds a filename with the given base and the given extension, possibly * Builds a filename with the given base and the given extension, possibly adding a suffix to ensure the file doesn't exist.
* adding a suffix to ensure the file doesn't exist.
* *
* @param directory the directory the filename will be located in * @param directory the directory the filename will be located in
* @param base the base for the filename * @param base the base for the filename
@@ -182,8 +180,7 @@ public class FileUtils {
* @param suffix the first numeric suffix to try to use, or 0 for none * @param suffix the first numeric suffix to try to use, or 0 for none
* @return the complete filename, without the directory * @return the complete filename, without the directory
*/ */
private static String buildUniqueFileName( private static String buildUniqueFileName(File directory, String base, String extension, int suffix) {
File directory, String base, String extension, int suffix) {
String suffixName = ""; String suffixName = "";
if (suffix > 0) { if (suffix > 0) {
suffixName += "(" + suffix + ")"; suffixName += "(" + suffix + ")";
@@ -201,10 +198,9 @@ public class FileUtils {
} }
/** /**
* Sanitizes the name as a valid fat32 filename. For simplicity, fat32 * Sanitizes the name as a valid fat32 filename.
* filename characters may be any combination of letters, digits, or * For simplicity, fat32 filename characters may be any combination of letters, digits, or characters with code point values greater than 127.
* characters with code point values greater than 127. Replaces the invalid * Replaces the invalid characters with "_" and collapses multiple "_" together.
* characters with "_" and collapses multiple "_" together.
* *
* @param name name * @param name name
*/ */
@@ -260,8 +256,7 @@ public class FileUtils {
} }
/** /**
* Truncates the name if necessary so the filename path length (directory + * Truncates the name if necessary so the filename path length (directory + name + suffix) meets the Fat32 path limit.
* name + suffix) meets the Fat32 path limit.
* *
* @param directory directory * @param directory directory
* @param name name * @param name name
@@ -273,8 +268,8 @@ public class FileUtils {
if (name.length() + requiredLength > MAX_FAT32_PATH_LENGTH) { if (name.length() + requiredLength > MAX_FAT32_PATH_LENGTH) {
int limit = MAX_FAT32_PATH_LENGTH - requiredLength; int limit = MAX_FAT32_PATH_LENGTH - requiredLength;
return name.substring(0, limit); return name.substring(0, limit);
} else {
return name;
} }
return name;
} }
} }
@@ -48,12 +48,13 @@ public class IntentUtils {
private final static String TAG = IntentUtils.class.getSimpleName(); private final static String TAG = IntentUtils.class.getSimpleName();
private static final String JPEG_EXTENSION = "jpeg";
private IntentUtils() { private IntentUtils() {
} }
/** /**
* Creates an intent with {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} and * Creates an intent with {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} and {@link Intent#FLAG_ACTIVITY_NEW_TASK}.
* {@link Intent#FLAG_ACTIVITY_NEW_TASK}.
* *
* @param context the context * @param context the context
* @param cls the class * @param cls the class
@@ -62,12 +63,10 @@ public class IntentUtils {
return new Intent(context, cls).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); return new Intent(context, cls).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
} }
private static final String JPEG_EXTENSION = "jpeg";
/** /**
* Creates an intent to share a track file with an app. * Creates an intent to share a track file with an app.
* *
* @param context the context * @param context the context
* @param trackIds the track ids * @param trackIds the track ids
*/ */
public static Intent newShareFileIntent(Context context, long[] trackIds) { public static Intent newShareFileIntent(Context context, long[] trackIds) {
@@ -90,6 +89,14 @@ public class IntentUtils {
showCoordinateOnMap(context, waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName()); showCoordinateOnMap(context, waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName());
} }
/**
* Send intent to show coordinates on a map (needs an another app).
*
* @param context the context
* @param latitude the latitude
* @param longitude the longitude
* @param label the label
*/
public static void showCoordinateOnMap(Context context, double latitude, double longitude, String label) { public static void showCoordinateOnMap(Context context, double latitude, double longitude, String label) {
//SEE https://developer.android.com/guide/components/intents-common.html#Maps //SEE https://developer.android.com/guide/components/intents-common.html#Maps
String uri = "geo:0,0?q=" + latitude + "," + longitude; String uri = "geo:0,0?q=" + latitude + "," + longitude;
@@ -61,11 +61,7 @@ public class ListItemUtils {
* @param description the description value * @param description the description value
* @param photoUrl the photo url * @param photoUrl the photo url
*/ */
public static void setListItem(Activity activity, View view, boolean isRecording, public static void setListItem(Activity activity, View view, boolean isRecording, boolean isPaused, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, long startTime, boolean useRelativeTime, String category, String description, String photoUrl) {
boolean isPaused, int iconId, int iconContentDescriptionId, String name,
String totalTime, String totalDistance, int markerCount, long startTime,
boolean useRelativeTime, String category, String description, String photoUrl) {
// Set photo // Set photo
ImageView photo = view.findViewById(R.id.list_item_photo); ImageView photo = view.findViewById(R.id.list_item_photo);
ImageView textGradient = view.findViewById(R.id.list_item_text_gradient); ImageView textGradient = view.findViewById(R.id.list_item_text_gradient);
@@ -134,17 +130,11 @@ public class ListItemUtils {
setTextView(activity, timeTextView, dateTime[1], hasPhoto); setTextView(activity, timeTextView, dateTime[1], hasPhoto);
// Set category and description // Set category and description
TextView categoryDescriptionTextView = view.findViewById( TextView categoryDescriptionTextView = view.findViewById(R.id.list_item_category_description);
R.id.list_item_category_description); String categoryDescription = isRecording ? null : StringUtils.getCategoryDescription(category, description);
String categoryDescription = isRecording ? null
: StringUtils.getCategoryDescription(category, description);
/* // Place categoryDescription in either ownerTimeDistanceTextView or categoryDescriptionTextView
* Place categoryDescription in either ownerTimeDistanceTextView or if (timeDistanceTextView.getVisibility() == View.GONE && markerCountIcon.getVisibility() == View.GONE) {
* categoryDescriptionTextView
*/
if (timeDistanceTextView.getVisibility() == View.GONE
&& markerCountIcon.getVisibility() == View.GONE) {
setTextView(activity, categoryDescriptionTextView, null, hasPhoto); setTextView(activity, categoryDescriptionTextView, null, hasPhoto);
// Match list_item_category_description in list_item.xml // Match list_item_category_description in list_item.xml
timeDistanceTextView.setSingleLine(false); timeDistanceTextView.setSingleLine(false);
@@ -158,8 +148,7 @@ public class ListItemUtils {
// Adjust iconImageView layout gravity // Adjust iconImageView layout gravity
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) iconImageView.getLayoutParams(); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) iconImageView.getLayoutParams();
params.gravity = timeDistanceTextView.getVisibility() == View.GONE params.gravity = timeDistanceTextView.getVisibility() == View.GONE && markerCountIcon.getVisibility() == View.GONE ? Gravity.TOP : Gravity.CENTER_VERTICAL;
&& markerCountIcon.getVisibility() == View.GONE ? Gravity.TOP : Gravity.CENTER_VERTICAL;
} }
/** /**
@@ -196,22 +185,17 @@ public class ListItemUtils {
if (isRecording || time == 0L) { if (isRecording || time == 0L) {
return new String[]{null, null}; return new String[]{null, null};
} }
boolean isToday = DateUtils.isToday(time); boolean isToday = DateUtils.isToday(time);
int timeFlags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL; int timeFlags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL;
if (isToday) { if (isToday && useRelativeTime) {
if (useRelativeTime) { return new String[]{DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE).toString(), null};
return new String[]{DateUtils.getRelativeTimeSpanString( } else if (isToday) {
time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, return new String[]{DateUtils.formatDateTime(context, time, timeFlags), null};
DateUtils.FORMAT_ABBREV_RELATIVE).toString(), null};
} else {
return new String[]{DateUtils.formatDateTime(context, time, timeFlags), null};
}
} }
return new String[]{DateUtils.formatDateTime( return new String[]{DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL), DateUtils.formatDateTime(context, time, timeFlags)};
context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL),
DateUtils.formatDateTime(context, time, timeFlags)};
} }
/** /**
@@ -225,14 +209,15 @@ public class ListItemUtils {
public static void setTextView(Context context, TextView textView, String value, boolean addShadow) { public static void setTextView(Context context, TextView textView, String value, boolean addShadow) {
if (value == null || value.length() == 0) { if (value == null || value.length() == 0) {
textView.setVisibility(View.GONE); textView.setVisibility(View.GONE);
return;
}
textView.setVisibility(View.VISIBLE);
textView.setText(value);
if (addShadow) {
textView.setShadowLayer(5, 0, 2, context.getResources().getColor(android.R.color.black));
} else { } else {
textView.setVisibility(View.VISIBLE); textView.setShadowLayer(0, 0, 0, 0);
textView.setText(value);
if (addShadow) {
textView.setShadowLayer(5, 0, 2, context.getResources().getColor(android.R.color.black));
} else {
textView.setShadowLayer(0, 0, 0, 0);
}
} }
} }
@@ -18,30 +18,30 @@ package de.dennisguse.opentracks.util;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import de.dennisguse.opentracks.content.Track;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Stack; import java.util.Stack;
import de.dennisguse.opentracks.content.Track;
/** /**
* Utility class for decimating tracks at a given level of precision. * Utility class for decimating tracks at a given level of precision.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
//TODO Cleanup: rename variables
public class LocationUtils { public class LocationUtils {
private static final String TAG = LocationUtils.class.getSimpleName(); private static final String TAG = LocationUtils.class.getSimpleName();
// 1 minute in milliseconds // 1 minute in milliseconds
private static final long MAX_LOCATION_AGE_MS = (long) (UnitConversions.MIN_TO_S private static final long MAX_LOCATION_AGE_MS = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
* UnitConversions.S_TO_MS);
private LocationUtils() { private LocationUtils() {
} }
/** /**
* Computes the distance on the two sphere between the point c0 and the line * Computes the distance on the two sphere between the point c0 and the line segment c1 to c2.
* segment c1 to c2.
* *
* @param c0 the first coordinate * @param c0 the first coordinate
* @param c1 the beginning of the line segment * @param c1 the beginning of the line segment
@@ -64,31 +64,36 @@ public class LocationUtils {
double s2s1lng = s2lng - s1lng; double s2s1lng = s2lng - s1lng;
final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng) final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng); / (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
if (u <= 0) { if (u <= 0) {
return c0.distanceTo(c1); return c0.distanceTo(c1);
} }
if (u >= 1) { if (u >= 1) {
return c0.distanceTo(c2); return c0.distanceTo(c2);
} }
Location sa = new Location(""); Location sa = new Location("");
sa.setLatitude(c0.getLatitude() - c1.getLatitude()); sa.setLatitude(c0.getLatitude() - c1.getLatitude());
sa.setLongitude(c0.getLongitude() - c1.getLongitude()); sa.setLongitude(c0.getLongitude() - c1.getLongitude());
Location sb = new Location(""); Location sb = new Location("");
sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude())); sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude()));
sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude())); sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude()));
return sa.distanceTo(sb); return sa.distanceTo(sb);
} }
/** /**
* Decimates the given locations for a given zoom level. This uses a * Decimates the given locations for a given zoom level.
* Douglas-Peucker decimation algorithm. * This uses a Douglas-Peucker decimation algorithm.
* *
* @param tolerance in meters * @param tolerance in meters
* @param locations input * @param locations input
* @param decimated output * @param decimated output
*/ */
private static void decimate( //TODO What was it used for? Sharing data with other apps?
double tolerance, ArrayList<Location> locations, ArrayList<Location> decimated) { private static void decimate(double tolerance, List<Location> locations, List<Location> decimated) {
final int n = locations.size(); final int n = locations.size();
if (n < 1) { if (n < 1) {
return; return;
@@ -153,16 +158,16 @@ public class LocationUtils {
} }
/** /**
* Checks if a given location is a valid (i.e. physically possible) location * Checks if a given location is a valid (i.e. physically possible) locationon Earth.
* on Earth. Note: The special separator locations (which have latitude = 100) * Note: The special separator locations (which have latitude = 100) will not qualify as valid.
* will not qualify as valid. Neither will locations with lat=0 and lng=0 as * Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble.
* these are most likely "bad" measurements which often cause trouble.
* *
* @param location the location to test * @param location the location to test
* @return true if the location is a valid location. * @return true if the location is a valid location.
*/ */
public static boolean isValidLocation(Location location) { public static boolean isValidLocation(Location location) {
return location != null && Math.abs(location.getLatitude()) <= 90 return location != null
&& Math.abs(location.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180; && Math.abs(location.getLongitude()) <= 180;
} }
@@ -172,7 +177,6 @@ public class LocationUtils {
* @param location the location * @param location the location
*/ */
public static boolean isLocationOld(Location location) { public static boolean isLocationOld(Location location) {
return !LocationUtils.isValidLocation(location) return !LocationUtils.isValidLocation(location) || (System.currentTimeMillis() - location.getTime() > MAX_LOCATION_AGE_MS);
|| (System.currentTimeMillis() - location.getTime() > MAX_LOCATION_AGE_MS);
} }
} }
@@ -31,8 +31,8 @@ import de.dennisguse.opentracks.R;
public class PreferencesUtils { public class PreferencesUtils {
/* /*
* Preferences values. The defaults need to match the defaults in the xml * Preferences values.
* files. * The defaults need to match the defaults in the xml files.
*/ */
public static final int AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT = 0; public static final int AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT = 0;
@@ -48,7 +48,10 @@ public class PreferencesUtils {
public static final boolean CHART_SHOW_HEART_RATE_DEFAULT = true; public static final boolean CHART_SHOW_HEART_RATE_DEFAULT = true;
public static final boolean CHART_SHOW_POWER_DEFAULT = true; public static final boolean CHART_SHOW_POWER_DEFAULT = true;
public static final boolean CHART_SHOW_SPEED_DEFAULT = true; public static final boolean CHART_SHOW_SPEED_DEFAULT = true;
public static final String CHART_X_AXIS_DEFAULT = "DISTANCE"; @Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This is also used to recover from a reboot, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static final String DEFAULT_ACTIVITY_DEFAULT = ""; public static final String DEFAULT_ACTIVITY_DEFAULT = "";
@@ -68,20 +71,16 @@ public class PreferencesUtils {
public static final int RECORDING_GPS_ACCURACY_DEFAULT = 50; public static final int RECORDING_GPS_ACCURACY_DEFAULT = 50;
public static final int RECORDING_GPS_ACCURACY_EXCELLENT = 10; public static final int RECORDING_GPS_ACCURACY_EXCELLENT = 10;
public static final int RECORDING_GPS_ACCURACY_POOR = 2000; public static final int RECORDING_GPS_ACCURACY_POOR = 2000;
static final boolean STATS_SHOW_COORDINATE_DEFAULT = false;
@Deprecated //NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static final boolean RECORDING_TRACK_PAUSED_DEFAULT = true; public static final boolean RECORDING_TRACK_PAUSED_DEFAULT = true;
public static final int SPLIT_FREQUENCY_DEFAULT = 0; public static final int SPLIT_FREQUENCY_DEFAULT = 0;
static final boolean STATS_SHOW_ELEVATION_DEFAULT = false;
// Stats static final String TRACK_NAME_DEFAULT = "DATE_ISO_8601";
public static final String STATS_RATE_DEFAULT = "SPEED"; private static final String CHART_X_AXIS_DEFAULT = "DISTANCE";
public static final boolean STATS_SHOW_COORDINATE_DEFAULT = false;
public static final boolean STATS_SHOW_ELEVATION_DEFAULT = false;
public static final String STATS_UNITS_DEFAULT = "METRIC"; public static final String STATS_UNITS_DEFAULT = "METRIC";
// Stats
public static final String TRACK_NAME_DEFAULT = "DATE_ISO_8601"; private static final String STATS_RATE_DEFAULT = "SPEED";
public static final boolean SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN = false; public static final boolean SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN = false;
@@ -47,10 +47,8 @@ public class StatsUtils {
* Sets the location values. * Sets the location values.
* *
* @param context the context * @param context the context
* @param activity the activity for finding views. If null, the view cannot be * @param activity the activity for finding views. If null, the view cannot be null
* null * @param view the containing view for finding views. If null, the activity cannot be null
* @param view the containing view for finding views. If null, the activity
* cannot be null
* @param location the location * @param location the location
* @param isRecording true if recording * @param isRecording true if recording
*/ */
@@ -96,19 +94,17 @@ public class StatsUtils {
} }
public static void setSensorData(Context context, Activity activity, SensorDataSet sensorDataSet, boolean isRecording) { public static void setSensorData(Context context, Activity activity, SensorDataSet sensorDataSet, boolean isRecording) {
{ // SensorDataSet: heart rate
// SensorDataSet: heart rate int isVisible = View.VISIBLE;
int isVisible = View.VISIBLE; if (!isRecording || PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT))) {
if (!isRecording || PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT))) { isVisible = View.INVISIBLE;
isVisible = View.INVISIBLE; }
} getView(activity, null, R.id.stats_sensor_container).setVisibility(isVisible);
getView(activity, null, R.id.stats_sensor_container).setVisibility(isVisible);
View viewValue = getView(activity, null, R.id.stats_sensor_heart_rate_value); View viewValue = getView(activity, null, R.id.stats_sensor_heart_rate_value);
View viewSensor = getView(activity, null, R.id.stats_sensor_heart_rate_sensor); View viewSensor = getView(activity, null, R.id.stats_sensor_heart_rate_sensor);
if (isRecording) { if (isRecording) {
setHeartRateData(context, viewValue, viewSensor, sensorDataSet); setHeartRateData(context, viewValue, viewSensor, sensorDataSet);
}
} }
} }
@@ -126,13 +122,10 @@ public class StatsUtils {
* Sets the trip statistics values. * Sets the trip statistics values.
* *
* @param context the context * @param context the context
* @param activity the activity for finding views. If null, then view cannot * @param activity the activity for finding views. If null, then view cannot be null
* be null * @param view the containing view for finding views. If null, the activity cannot be null
* @param view the containing view for finding views. If null, the activity
* cannot be null
* @param tripStatistics the trip statistics * @param tripStatistics the trip statistics
* @param trackIconValue the track icon value or null to hide the track icon * @param trackIconValue the track icon value or null to hide the track icon spinner
* spinner
*/ */
public static void setTripStatisticsValues(Context context, Activity activity, View view, TripStatistics tripStatistics, String trackIconValue) { public static void setTripStatisticsValues(Context context, Activity activity, View view, TripStatistics tripStatistics, String trackIconValue) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
@@ -265,8 +258,7 @@ public class StatsUtils {
* @param grade the grade in fraction between 0 and 1 * @param grade the grade in fraction between 0 and 1
*/ */
private static void setGradeValue(Context context, View view, int labelId, double grade) { private static void setGradeValue(Context context, View view, int labelId, double grade) {
String value = Double.isNaN(grade) || Double.isInfinite(grade) ? null String value = Double.isNaN(grade) || Double.isInfinite(grade) ? null : String.format(Locale.getDefault(), GRADE_FORMAT, Math.round(grade * 100));
: String.format(Locale.getDefault(), GRADE_FORMAT, Math.round(grade * 100));
setItem(context, view, labelId, value, GRADE_PERCENTAGE); setItem(context, view, labelId, value, GRADE_PERCENTAGE);
} }
@@ -279,8 +271,7 @@ public class StatsUtils {
* @param coordinate the coordinate in degrees * @param coordinate the coordinate in degrees
*/ */
private static void setCoordinateValue(Context context, View view, int labelId, double coordinate) { private static void setCoordinateValue(Context context, View view, int labelId, double coordinate) {
String value = Double.isNaN(coordinate) || Double.isInfinite(coordinate) ? null String value = Double.isNaN(coordinate) || Double.isInfinite(coordinate) ? null : StringUtils.formatCoordinate(coordinate);
: StringUtils.formatCoordinate(coordinate);
setItem(context, view, labelId, value, null); setItem(context, view, labelId, value, null);
} }
@@ -17,13 +17,9 @@ package de.dennisguse.opentracks.util;
import android.content.Context; import android.content.Context;
import android.location.Location; import android.location.Location;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import de.dennisguse.opentracks.R;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
@@ -32,6 +28,8 @@ import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import de.dennisguse.opentracks.R;
/** /**
* Various string manipulation methods. * Various string manipulation methods.
* *
@@ -41,18 +39,17 @@ import java.util.regex.Pattern;
public class StringUtils { public class StringUtils {
private static final String COORDINATE_DEGREE = "\u00B0"; private static final String COORDINATE_DEGREE = "\u00B0";
private static final SimpleDateFormat ISO_8601_DATE_TIME_FORMAT = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); private static final SimpleDateFormat ISO_8601_DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private static final SimpleDateFormat ISO_8601_BASE = new SimpleDateFormat( private static final SimpleDateFormat ISO_8601_BASE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
"yyyy-MM-dd'T'HH:mm:ss", Locale.US); private static final Pattern ISO_8601_EXTRAS = Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
private static final Pattern ISO_8601_EXTRAS = Pattern.compile(
"^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
static { static {
ISO_8601_DATE_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); ISO_8601_DATE_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
ISO_8601_BASE.setTimeZone(TimeZone.getTimeZone("UTC")); ISO_8601_BASE.setTimeZone(TimeZone.getTimeZone("UTC"));
} }
private StringUtils() { private StringUtils() {
} }
@@ -63,9 +60,8 @@ public class StringUtils {
* @param time the time in milliseconds * @param time the time in milliseconds
*/ */
public static String formatDateTime(Context context, long time) { public static String formatDateTime(Context context, long time) {
return DateUtils.formatDateTime( return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE)
context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE) + " " + " " + DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_TIME);
+ DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_TIME);
} }
/** /**
@@ -80,19 +76,20 @@ public class StringUtils {
/** /**
* Formats the elapsed timed in the form "MM:SS" or "H:MM:SS". * Formats the elapsed timed in the form "MM:SS" or "H:MM:SS".
* TODO Remove as min API level is now 21; replace with DateUtils.formatElapsedTime(time * MS_TO_S).
* *
* @param time the time in milliseconds * @param time the time in milliseconds
*/ */
public static String formatElapsedTime(long time) { public static String formatElapsedTime(long time) {
DateUtils.formatElapsedTime(1);
/* /*
* TODO * Temporary workaround for DateUtils.formatElapsedTime(time * MS_TO_S).
* Temporary workaround for DateUtils.formatElapsedTime(time * MS_TO_S). In API * In API level 17, it returns strings like "1:0:00" instead of "1:00:00", which breaks several unit tests.
* level 17, it returns strings like "1:0:00" instead of "1:00:00", which
* breaks several unit tests.
*/ */
if (time < 0) { if (time < 0) {
return "-"; return "-";
} }
long hours = 0; long hours = 0;
long minutes = 0; long minutes = 0;
long elapsedSeconds = (long) (time * UnitConversions.MS_TO_S); long elapsedSeconds = (long) (time * UnitConversions.MS_TO_S);
@@ -135,6 +132,7 @@ public class StringUtils {
if (Double.isNaN(distance) || Double.isInfinite(distance)) { if (Double.isNaN(distance) || Double.isInfinite(distance)) {
return context.getString(R.string.value_unknown); return context.getString(R.string.value_unknown);
} }
if (metricUnits) { if (metricUnits) {
if (distance > 500.0) { if (distance > 500.0) {
distance *= UnitConversions.M_TO_KM; distance *= UnitConversions.M_TO_KM;
@@ -153,11 +151,11 @@ public class StringUtils {
} }
} }
public static String formatDecimal(double value) { static String formatDecimal(double value) {
return StringUtils.formatDecimal(value, 2); return StringUtils.formatDecimal(value, 2);
} }
public static String formatDecimal(double value, int decimalPlaces) { static String formatDecimal(double value, int decimalPlaces) {
String result = String.format(Locale.getDefault(), "%1$,." + decimalPlaces + "f", value); String result = String.format(Locale.getDefault(), "%1$,." + decimalPlaces + "f", value);
return result.replaceAll("[0]*$", "").replaceAll("\\.$", ""); return result.replaceAll("[0]*$", "").replaceAll("\\.$", "");
} }
@@ -172,14 +170,16 @@ public class StringUtils {
} }
/** /**
* Gets the distance in an array of two strings. The first string is the * Gets the distance in an array of two strings.
* distance. The second string is the unit. The first string is null if the * The first string is the distance.
* distance is invalid. * The second string is the unit.
* The first string is null if the distance is invalid.
* *
* @param context the context * @param context the context
* @param distance the distance * @param distance the distance
* @param metricUnits true to use metric unit * @param metricUnits true to use metric unit
*/ */
//TODO Return Pair<Distance, Unit>
public static String[] getDistanceParts(Context context, double distance, boolean metricUnits) { public static String[] getDistanceParts(Context context, double distance, boolean metricUnits) {
String[] result = new String[2]; String[] result = new String[2];
if (Double.isNaN(distance) || Double.isInfinite(distance)) { if (Double.isNaN(distance) || Double.isInfinite(distance)) {
@@ -211,17 +211,18 @@ public class StringUtils {
} }
/** /**
* Gets the speed in an array of two strings. The first string is the speed. * Gets the speed in an array of two strings.
* The second string is the unit. The first string is null if speed is * The first string is the speed.
* invalid. * The second string is the unit.
* The first string is null if speed is invalid.
* *
* @param context the context * @param context the context
* @param speed the speed * @param speed the speed
* @param metricUnits true to use metric unit * @param metricUnits true to use metric unit
* @param reportSpeed true to report speed * @param reportSpeed true to report speed
*/ */
public static String[] getSpeedParts( //TODO Return Pair<Distance, Unit>
Context context, double speed, boolean metricUnits, boolean reportSpeed) { public static String[] getSpeedParts(Context context, double speed, boolean metricUnits, boolean reportSpeed) {
String[] result = new String[2]; String[] result = new String[2];
int unitId; int unitId;
if (metricUnits) { if (metricUnits) {
@@ -268,7 +269,7 @@ public class StringUtils {
* @param category the category * @param category the category
* @param description the description * @param description the description
*/ */
public static String getCategoryDescription(String category, String description) { static String getCategoryDescription(String category, String description) {
if (category == null || category.length() == 0) { if (category == null || category.length() == 0) {
return description; return description;
} }
@@ -282,9 +283,9 @@ public class StringUtils {
} }
/** /**
* Formats the given text as a XML CDATA element. This includes adding the * Formats the given text as a XML CDATA element.
* starting and ending CDATA tags. Please notice that this may result in * This includes adding the starting and ending CDATA tags.
* multiple consecutive CDATA tags. * NOTE: This may result in multiple consecutive CDATA tags.
* *
* @param text the given text * @param text the given text
*/ */
@@ -293,25 +294,23 @@ public class StringUtils {
} }
/** /**
* Gets the time, in milliseconds, from an XML date time string as defined at * Gets the time, in milliseconds, from an XML date time string as defined at http://www.w3.org/TR/xmlschema-2/#dateTime
* http://www.w3.org/TR/xmlschema-2/#dateTime
* *
* @param xmlDateTime the XML date time string * @param xmlDateTime the XML date time string
*/ */
//TODO Can this be replaced using java.time?
public static long getTime(String xmlDateTime) { public static long getTime(String xmlDateTime) {
// Parse the date time base // Parse the date time base
ParsePosition position = new ParsePosition(0); ParsePosition position = new ParsePosition(0);
Date date = ISO_8601_BASE.parse(xmlDateTime, position); Date date = ISO_8601_BASE.parse(xmlDateTime, position);
if (date == null) { if (date == null) {
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime + " (at position " + position.getErrorIndex() + ")");
+ " (at position " + position.getErrorIndex() + ")");
} }
// Parse the date time extras // Parse the date time extras
Matcher matcher = ISO_8601_EXTRAS.matcher(xmlDateTime.substring(position.getIndex())); Matcher matcher = ISO_8601_EXTRAS.matcher(xmlDateTime.substring(position.getIndex()));
if (!matcher.matches()) { if (!matcher.matches()) {
// This will match even an empty string as all groups are optional. Thus a // This will match even an empty string as all groups are optional. Thus a non-match means invalid content.
// non-match means invalid content.
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime); throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlDateTime);
} }
@@ -354,9 +353,8 @@ public class StringUtils {
} }
/** /**
* Gets the time as an array of three integers. Index 0 contains the number of * Gets the time as an array of three integers.
* seconds, index 1 contains the number of minutes, and index 2 contains the * Index 0 contains the number of seconds, index 1 contains the number of minutes, and index 2 contains the number of hours.
* number of hours.
* *
* @param time the time in milliseconds * @param time the time in milliseconds
* @return an array of 3 elements. * @return an array of 3 elements.
@@ -379,22 +377,6 @@ public class StringUtils {
return parts; return parts;
} }
/**
* Gets the html.
*
* @param context the context
* @param resId the string resource id
* @param formatArgs the string resource ids of the format arguments
*/
public static Spanned getHtml(Context context, int resId, Object... formatArgs) {
Object[] args = new Object[formatArgs.length];
for (int i = 0; i < formatArgs.length; i++) {
String url = context.getString((Integer) formatArgs[i]);
args[i] = " <a href='" + url + "'>" + url + "</a> ";
}
return Html.fromHtml(context.getString(resId, args));
}
/** /**
* Gets the frequency display options. * Gets the frequency display options.
* *
@@ -409,9 +391,7 @@ public class StringUtils {
if (value == PreferencesUtils.FREQUENCY_OFF) { if (value == PreferencesUtils.FREQUENCY_OFF) {
options[i] = context.getString(R.string.value_off); options[i] = context.getString(R.string.value_off);
} else if (value < 0) { } else if (value < 0) {
options[i] = context.getString( options[i] = context.getString(metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile, Math.abs(value));
metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile,
Math.abs(value));
} else { } else {
options[i] = context.getString(R.string.value_integer_minute, value); options[i] = context.getString(R.string.value_integer_minute, value);
} }
@@ -33,12 +33,12 @@ import android.widget.Spinner;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R;
/** /**
* Utilities for track icon. * Utilities for track icon.
* *
@@ -55,31 +55,16 @@ public class TrackIconUtils {
private static final String SNOW_BOARDING = "SNOW_BOARDING"; private static final String SNOW_BOARDING = "SNOW_BOARDING";
private static final String WALK = "WALK"; private static final String WALK = "WALK";
private static final int[] AIRPLANE_LIST = new int[]{R.string.activity_type_airplane, private static final int[] AIRPLANE_LIST = new int[]{R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane};
R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane}; private static final int[] BIKE_LIST = new int[]{R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_motor_bike, R.string.activity_type_mountain_biking, R.string.activity_type_road_biking, R.string.activity_type_track_cycling};
private static final int[] BIKE_LIST = new int[]{R.string.activity_type_biking, private static final int[] BOAT_LIST = new int[]{R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat};
R.string.activity_type_cycling, R.string.activity_type_dirt_bike, private static final int[] DRIVE_LIST = new int[]{R.string.activity_type_atv, R.string.activity_type_driving, R.string.activity_type_driving_bus, R.string.activity_type_driving_car};
R.string.activity_type_motor_bike, R.string.activity_type_mountain_biking, private static final int[] RUN_LIST = new int[]{R.string.activity_type_running, R.string.activity_type_street_running, R.string.activity_type_track_running, R.string.activity_type_trail_running};
R.string.activity_type_road_biking, R.string.activity_type_track_cycling}; private static final int[] SKI_LIST = new int[]{R.string.activity_type_cross_country_skiing, R.string.activity_type_skiing};
private static final int[] BOAT_LIST = new int[]{R.string.activity_type_boat, private static final int[] SNOW_BOARDING_LIST = new int[]{R.string.activity_type_snow_boarding};
R.string.activity_type_ferry, R.string.activity_type_motor_boating, private static final int[] WALK_LIST = new int[]{R.string.activity_type_hiking, R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking, R.string.activity_type_trail_hiking, R.string.activity_type_walking};
R.string.activity_type_rc_boat};
private static final int[] DRIVE_LIST = new int[]{R.string.activity_type_atv,
R.string.activity_type_driving, R.string.activity_type_driving_bus,
R.string.activity_type_driving_car};
private static final int[] RUN_LIST = new int[]{R.string.activity_type_running,
R.string.activity_type_street_running, R.string.activity_type_track_running,
R.string.activity_type_trail_running};
private static final int[] SKI_LIST = new int[]{
R.string.activity_type_cross_country_skiing, R.string.activity_type_skiing};
private static final int[] SNOW_BOARDING_LIST = new int[]{
R.string.activity_type_snow_boarding};
private static final int[] WALK_LIST = new int[]{R.string.activity_type_hiking,
R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking,
R.string.activity_type_trail_hiking, R.string.activity_type_walking};
private static final LinkedHashMap<String, Pair<Integer, Integer>> private static final LinkedHashMap<String, Pair<Integer, Integer>> MAP = new LinkedHashMap<>();
MAP = new LinkedHashMap<>();
static { static {
MAP.put(RUN, new Pair<>(R.string.activity_type_running, R.drawable.ic_track_run)); MAP.put(RUN, new Pair<>(R.string.activity_type_running, R.drawable.ic_track_run));
@@ -173,10 +158,8 @@ public class TrackIconUtils {
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
public static ArrayAdapter<StringBuilder> getIconSpinnerAdapter( public static ArrayAdapter<StringBuilder> getIconSpinnerAdapter(final Context context, String iconValue) {
final Context context, String iconValue) { return new ArrayAdapter<StringBuilder>(context, android.R.layout.simple_spinner_item, new StringBuilder[]{new StringBuilder(iconValue)}) {
return new ArrayAdapter<StringBuilder>(context, android.R.layout.simple_spinner_item,
new StringBuilder[]{new StringBuilder(iconValue)}) {
@NonNull @NonNull
@Override @Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
@@ -211,8 +194,7 @@ public class TrackIconUtils {
* @param menu the menu * @param menu the menu
*/ */
public static void setMenuIconColor(Menu menu) { public static void setMenuIconColor(Menu menu) {
int size = menu.size(); for (int i = 0; i < menu.size(); i++) {
for (int i = 0; i < size; i++) {
MenuItem menuitem = menu.getItem(i); MenuItem menuitem = menu.getItem(i);
setMenuIconColor(menuitem); setMenuIconColor(menuitem);
} }
@@ -32,6 +32,7 @@ import de.dennisguse.opentracks.R;
*/ */
public class TrackNameUtils { public class TrackNameUtils {
//TODO Could be available in java.time?
@VisibleForTesting @VisibleForTesting
static final String ISO_8601_FORMAT = "yyyy-MM-dd HH:mm"; static final String ISO_8601_FORMAT = "yyyy-MM-dd HH:mm";
@@ -24,14 +24,14 @@ import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackEditActivity; import de.dennisguse.opentracks.TrackEditActivity;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.R;
import java.util.List;
/** /**
* Utilities for {@link TrackRecordingServiceConnection}. * Utilities for {@link TrackRecordingServiceConnection}.
@@ -52,6 +52,11 @@ public class TrackRecordingServiceConnectionUtils {
*/ */
public static boolean isRecordingServiceRunning(Context context) { public static boolean isRecordingServiceRunning(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return false;
}
//TODO This approach is deprecated as of API level 26 and should be replaced.
List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE); List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo serviceInfo : services) { for (RunningServiceInfo serviceInfo : services) {
@@ -99,13 +104,12 @@ public class TrackRecordingServiceConnectionUtils {
*/ */
public static void stopRecording(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean showEditor) { public static void stopRecording(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean showEditor) {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) { if (trackRecordingService == null) {
resetRecordingState(context);
} else {
try { try {
if (showEditor) { if (showEditor) {
/* // Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L.
* Need to remember the recordingTrackId before calling
* endCurrentTrack. endCurrentTrack sets the value to -1L.
*/
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
trackRecordingService.endCurrentTrack(); trackRecordingService.endCurrentTrack();
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
@@ -118,10 +122,9 @@ public class TrackRecordingServiceConnectionUtils {
trackRecordingService.endCurrentTrack(); trackRecordingService.endCurrentTrack();
} }
} catch (Exception e) { } catch (Exception e) {
//TODO What exception are we catching here? Should be removed...
Log.e(TAG, "Unable to stop recording.", e); Log.e(TAG, "Unable to stop recording.", e);
} }
} else {
resetRecordingState(context);
} }
trackRecordingServiceConnection.unbindAndStop(); trackRecordingServiceConnection.unbindAndStop();
} }
@@ -130,8 +133,7 @@ public class TrackRecordingServiceConnectionUtils {
* Resumes the track recording service connection. * Resumes the track recording service connection.
* *
* @param context the context * @param context the context
* @param trackRecordingServiceConnection the track recording service * @param trackRecordingServiceConnection the track recording service connection
* connection
*/ */
public static void startConnection(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection) { public static void startConnection(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection) {
trackRecordingServiceConnection.bindIfStarted(); trackRecordingServiceConnection.bindIfStarted();
@@ -143,25 +145,19 @@ public class TrackRecordingServiceConnectionUtils {
private static void resetRecordingState(Context context) { private static void resetRecordingState(Context context) {
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
PreferencesUtils.setLong( PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
} }
boolean recordingTrackPaused = PreferencesUtils.getBoolean(context, boolean recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!recordingTrackPaused) { if (!recordingTrackPaused) {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
} }
/** /**
* Adds a marker. * Adds a marker.
*/ */
public static long addMarker(Context context, public static long addMarker(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection, WaypointCreationRequest waypointCreationRequest) {
TrackRecordingServiceConnection trackRecordingServiceConnection, ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
WaypointCreationRequest waypointCreationRequest) {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
if (trackRecordingService == null) { if (trackRecordingService == null) {
Log.d(TAG, "Unable to add marker, no track recording service"); Log.d(TAG, "Unable to add marker, no track recording service");
} else { } else {
@@ -175,6 +171,7 @@ public class TrackRecordingServiceConnectionUtils {
Log.e(TAG, "Unable to add marker.", e); Log.e(TAG, "Unable to add marker.", e);
} }
} }
Toast.makeText(context, R.string.marker_add_error, Toast.LENGTH_LONG).show(); Toast.makeText(context, R.string.marker_add_error, Toast.LENGTH_LONG).show();
return -1L; return -1L;
} }
@@ -24,6 +24,7 @@ public class UnitConversions {
// multiplication factor to convert seconds to milliseconds // multiplication factor to convert seconds to milliseconds
public static final double S_TO_MS = 1000.0; public static final double S_TO_MS = 1000.0;
// Time // Time
// 1 second in milliseconds // 1 second in milliseconds
public static final long ONE_SECOND = (long) UnitConversions.S_TO_MS; public static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;

Some files were not shown because too many files have changed in this diff Show More