Compare commits

...

30 Commits

Author SHA1 Message Date
Dennis Guse b09eeaede3 Release v3.6.2 2020-04-13 12:56:59 +02:00
Dennis Guse 35dec1634c Bugfix: charts were showing only the invalid TrackPoints.
Introduced in fbc902e830
2020-04-13 12:48:52 +02:00
Dennis Guse e49a425106 Release v3.6.1 2020-04-12 18:46:49 +02:00
Román adbad04867 URIs for waypoint's photos are correct. Fixes #183. 2020-04-12 18:27:20 +02:00
Dennis Guse 850bd2f887 Update Android gradle plugin. 2020-04-11 23:16:38 +02:00
Dennis Guse d240b457e2 Release v3.6.0 2020-04-10 14:55:04 +02:00
Dennis Guse 6d800caab1 While importing photos, check url carefully.
(cherry picked from commit 309c96e4789bd7623b84720a8c8f2bf297c95d59)
2020-04-10 14:42:11 +02:00
Román 7bf153db39 Avoid NullPointer in creating photoUrl when importing 2020-04-10 14:42:11 +02:00
Román 1b69f29751 Importing creates content:// for attached pictures. Also, exporting without points bug fixed and images href contains relative link. Fixes #166 and #176. 2020-04-10 14:42:11 +02:00
Dennis Guse dfc3837739 Settings: PrefsFragment could not be re-created. Fixes #179. 2020-04-08 08:10:46 +02:00
Dennis Guse 30e847866e Removed TravisCI it does not work as desired. 2020-04-07 21:01:54 +02:00
Dennis Guse fbc902e830 Cleanup using AndroidStudio code analyzer.
Moved to iterator for loops for ChartView and ChartFragment.
2020-04-05 21:56:26 +02:00
Dennis Guse 208f1d71ba Refactor ChartView/ChartFragment to use ChartPoint. 2020-04-05 21:56:26 +02:00
Dennis Guse 38a15a96b3 Cleanup. 2020-04-05 21:56:26 +02:00
Dennis Guse 46f8753746 Removed TrackDataType and simplified TrackDataManager. 2020-04-05 21:56:26 +02:00
Dennis Guse 70bc8f33b1 Do not expose TrackDataType to fragments. 2020-04-05 21:56:26 +02:00
Dennis Guse 6cd27ab096 Merged DataSourceManager into TrackDataHub. 2020-04-05 21:56:26 +02:00
Dennis Guse 5581b404f7 DataSourceManager now always registers for all changes. 2020-04-05 21:56:26 +02:00
Dennis Guse 2ef74eb908 Automated code cleanup using AndroidStudio. 2020-04-05 21:56:26 +02:00
Dennis Guse 16c654141f Renamed TripStatistics to TrackStatistics. 2020-04-05 21:56:26 +02:00
Dennis Guse dfc785f2fd Simplified internal data processing (class structure). 2020-04-05 21:56:26 +02:00
Dennis Guse f621cdf032 Removed grade internals completely. 2020-04-05 21:56:26 +02:00
Román 8b04d9c4a6 creates a segment like pause/continue. 2020-04-05 14:25:29 +02:00
Dennis Guse 364b4702e3 Waypoint requires a non-null location.
Remove setLocation() from Waypoint and TrackPoint.
2020-03-30 19:52:06 +02:00
Dennis Guse a98c16de28 Removed TrackPointFactory.
New TrackPoints are now always created and old ones not reused.
2020-03-30 19:52:06 +02:00
Dennis Guse 881bf1982a Removed TrackWriterTrackPointFactory.
Replaced with local variable for TrackPoint swapping.
2020-03-30 19:52:06 +02:00
Dennis Guse f340622977 Automated code analysis improvements. 2020-03-30 19:52:06 +02:00
Dennis Guse 194379295d TrackPoint has Location as delegate instead of inheritance. 2020-03-30 19:52:06 +02:00
Dennis Guse 901cffee97 Use TrackPoint instead of Location (and omit casting). 2020-03-30 19:52:06 +02:00
Román d93c2ec5f3 TrackDetailActivity starts an actual recording and a stopped recording can be resumed. Fixes #103 and #11. 2020-03-30 19:25:22 +02:00
95 changed files with 1774 additions and 2205 deletions
-46
View File
@@ -1,46 +0,0 @@
#See https://docs.travis-ci.com/user/languages/android/
#See https://github.com/ankidroid/Anki-Android/blob/master/.travis.yml
language: android
env:
global:
- ADB_INSTALL_TIMEOUT=8
- ABI=armeabi-v7a
- EMU_FLAVOR=default
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- ANDROID_HOME=/usr/local/android-sdk
- TOOLS=${ANDROID_HOME}/tools
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=27 ABI=x86_64
android:
components:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3
- android-29
# Emulator Management: Create, Start and Wait
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script:
- ./gradlew build
- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
+1 -1
View File
@@ -4,7 +4,7 @@
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
3. Manually update _version code_ and _version name_ in `build.gradle`
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
+6 -3
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.2'
}
}
@@ -59,11 +59,14 @@ android {
applicationName: "@string/app_name"
]
//Useful for debugging.
//versionCode getVersionCode()
versionCode 3335
//versionCode getVersionCode() //Useful for debugging.
versionName "v3.6.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 29
@@ -0,0 +1,10 @@
v3.6.0: OpenTracks
Changes:
- Stopped tracks can now be resumed
Bugfix:
- Import of KMZ did not process the photo URL correctly
Developer:
- Simplified data processing
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Bugfix:
- KMZ import: store correct picture path
@@ -0,0 +1,4 @@
v3.6.2: OpenTracks
Bugfix:
- Charts were not shown correctly (introduced in v3.6.0)
+2 -1
View File
@@ -1,5 +1,6 @@
#Sun Apr 12 18:41:12 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -49,7 +49,7 @@ public class TrackStubUtils {
public static Track createTrack(int numberOfLocations) {
Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
track.addTrackPoint(createDefaultTrackPoint(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
@@ -60,8 +60,8 @@ public class TrackStubUtils {
*
* @return a track stub.
*/
public static TrackPoint createSensorDataSetLocation() {
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
public static TrackPoint createDefaultTrackPoint() {
return createDefaultTrackPoint(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
}
/**
@@ -69,18 +69,16 @@ public class TrackStubUtils {
*
* @return a SensorDataSetLocation stub.
*/
private static TrackPoint createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
loc.setLongitude(longitude);
loc.setAltitude(altitude);
loc.setAccuracy(INITIAL_ACCURACY);
loc.setSpeed(INITIAL_SPEED);
loc.setTime(INITIAL_TIME);
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(Float.NaN, Float.NaN);
private static TrackPoint createDefaultTrackPoint(double latitude, double longitude, double altitude) {
Location location = new Location(LOCATION_PROVIDER);
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAltitude(altitude);
location.setAccuracy(INITIAL_ACCURACY);
location.setSpeed(INITIAL_SPEED);
location.setTime(INITIAL_TIME);
location.setBearing(INITIAL_BEARING);
return new TrackPoint(loc, sd);
return new TrackPoint(location, new SensorDataSet(Float.NaN, Float.NaN));
}
}
@@ -15,6 +15,7 @@
*/
package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -44,7 +45,17 @@ public class ChartValueSeriesTest {
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border);
R.color.chart_elevation_border) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
};
}
@Test
@@ -63,8 +74,8 @@ public class ChartValueSeriesTest {
@Test
public void testSmallUpdates() {
series.update(0);
series.update(10);
series.update(new ChartPoint(0));
series.update(new ChartPoint(10));
series.updateDimension();
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
@@ -73,8 +84,8 @@ public class ChartValueSeriesTest {
@Test
public void testBigUpdates() {
series.update(0);
series.update(901);
series.update(new ChartPoint(0));
series.update(new ChartPoint(901));
series.updateDimension();
Assert.assertEquals(1000, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
@@ -83,8 +94,8 @@ public class ChartValueSeriesTest {
@Test
public void testNotZeroBasedUpdates() {
series.update(220);
series.update(250);
series.update(new ChartPoint(220));
series.update(new ChartPoint(250));
series.updateDimension();
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(200, series.getMinMarkerValue());
@@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -42,7 +42,7 @@ public class DescriptionGeneratorTest {
private static final long START_TIME = 1288721514000L;
private DescriptionGenerator descriptionGenerator;
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
@@ -55,7 +55,7 @@ public class DescriptionGeneratorTest {
@Test
public void testGenerateTrackDescription() {
Track track = new Track();
TripStatistics stats = new TripStatistics();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
@@ -63,10 +63,8 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42);
stats.setMinGrade(0.11);
stats.setStartTime(START_TIME);
track.setTripStatistics(stats);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
@@ -85,8 +83,6 @@ public class DescriptionGeneratorTest {
+ "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Max grade: 42 %<br>"
+ "Min grade: 11 %<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
@@ -142,34 +138,4 @@ public class DescriptionGeneratorTest {
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
Assert.assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteGrade() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(.042, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 4 %<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with a NaN.
*/
@Test
public void testWriteGrade_nan() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.NaN, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with an infinite number.
*/
@Test
public void testWriteGrade_infinite() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
}
}
@@ -39,7 +39,7 @@ import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for {@link SearchEngine}.
@@ -56,7 +56,7 @@ public class SearchEngineTest {
private ContentProviderUtils providerUtils;
private SearchEngine engine;
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
@@ -66,7 +66,7 @@ public class SearchEngineTest {
}
@After
public void tearDown() throws Exception {
public void tearDown() {
providerUtils.deleteAllTracks(context);
}
@@ -76,11 +76,11 @@ public class SearchEngineTest {
track.setDescription(description);
track.setCategory(category);
TripStatistics stats = track.getTripStatistics();
TrackStatistics stats = track.getTrackStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime(NOW);
stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime_ms(NOW);
}
Uri uri = providerUtils.insertTrack(track);
@@ -100,19 +100,17 @@ public class SearchEngineTest {
}
private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
Waypoint waypoint = new Waypoint();
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(location);
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Uri uri = providerUtils.insertWaypoint(waypoint);
return ContentUris.parseId(uri);
@@ -153,7 +151,7 @@ public class SearchEngineTest {
long allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
@@ -178,7 +176,7 @@ public class SearchEngineTest {
long allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
@@ -199,7 +197,7 @@ public class SearchEngineTest {
long trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
Assert.assertEquals(results.toString(), 3, results.size());
@@ -216,7 +214,7 @@ public class SearchEngineTest {
long farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
@@ -229,7 +227,7 @@ public class SearchEngineTest {
long oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
@@ -243,7 +241,7 @@ public class SearchEngineTest {
long oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
@@ -256,7 +254,7 @@ public class SearchEngineTest {
long otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
@@ -269,7 +267,7 @@ public class SearchEngineTest {
long currentId = insertWaypoint("ab", 1, 123);
SearchQuery query = new SearchQuery("a", null, 123, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
@@ -20,7 +20,7 @@ public class TestDataUtil {
track.setId(id);
track.setName("Test: " + id);
for (int i = 0; i < numPoints; i++) {
track.addLocation(createLocation(i));
track.addTrackPoint(createTrackPoint(i));
}
return track;
}
@@ -31,13 +31,13 @@ public class TestDataUtil {
* @param i the index to set the value of location.
* @return created location
*/
public static Location createLocation(int i) {
Location loc = new Location("test");
loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * ALTITUDE_INTERVAL);
loc.setTime(i + 1);
return loc;
public static TrackPoint createTrackPoint(int i) {
Location location = new Location("test");
location.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
location.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
location.setAccuracy((float) i / 100.0f);
location.setAltitude(i * ALTITUDE_INTERVAL);
location.setTime(i + 1);
return new TrackPoint(location);
}
}
@@ -38,7 +38,7 @@ import de.dennisguse.opentracks.content.data.WaypointsColumns;
public class CustomContentProviderTest {
private CustomContentProvider customContentProvider;
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
@@ -33,7 +33,6 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
@@ -42,7 +41,7 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.Mockito.when;
@@ -59,7 +58,7 @@ public class CustomContentProviderUtilsTest {
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Mock
@@ -76,70 +75,45 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_customFactory() {
final TrackPoint location = new TrackPoint("test_location");
final AtomicInteger counter = new AtomicInteger();
testIterator(1, 15, 4, false, new TrackPointFactory() {
@Override
public TrackPoint createLocation() {
counter.incrementAndGet();
return location;
}
});
// Make sure we were called exactly as many times as we had track points.
Assert.assertEquals(15, counter.get());
}
@Test
public void testLocationIterator_nullFactory() {
try {
testIterator(1, 15, 4, false, null);
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
}
testIterator(1, 0, 1, false);
}
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, false);
testIterator(2, 50, 50, false);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, true);
testIterator(2, 50, 50, true);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, false);
testIterator(2, 50, 25, false);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, true);
testIterator(2, 50, 25, true);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 20000, 2000, false);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, TrackPointFactory trackPointFactory) {
private void testIterator(long trackId, int numPoints, int batchSize, boolean descending) {
long lastPointId = initializeTrack(trackId, numPoints);
contentProviderUtils.setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, trackPointFactory)) {
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending)) {
while (it.hasNext()) {
Location loc = it.next();
TrackPoint loc = it.next();
Assert.assertNotNull(loc);
locations.add(loc);
// Make sure the IDs are returned in the right order.
@@ -148,7 +122,6 @@ public class CustomContentProviderUtilsTest {
}
Assert.assertEquals(numPoints, locations.size());
}
return locations;
}
private long initializeTrack(long id, int numPoints) {
@@ -159,21 +132,21 @@ public class CustomContentProviderUtilsTest {
track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Location[] locations = new Location[numPoints];
TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; ++i) {
Location loc = new Location("test");
loc.setLatitude(37.0 + (double) i / 10000.0);
loc.setLongitude(57.0 - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * 2.5);
locations[i] = loc;
trackPoints[i] = new TrackPoint(loc);
}
contentProviderUtils.bulkInsertTrackPoint(locations, numPoints, id);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, numPoints, id);
// Load all inserted locations.
// Load all inserted trackPoints.
long lastPointId = -1;
int counter = 0;
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getTrackPointId();
@@ -338,10 +311,9 @@ public class CustomContentProviderUtilsTest {
// Bottom
long startTime = 1000L;
// AverageSpeed
double minGrade = -20.11;
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(startTime);
statistics.setStopTime(2500L);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(startTime);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
@@ -349,22 +321,19 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(minGrade);
track.setTripStatistics(statistics);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
Location location = new Location("test");
location.setLatitude(22);
location.setLongitude(22);
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(location);
waypoint.setDescription(TEST_DESC);
contentProviderUtils.insertWaypoint(waypoint);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
@@ -444,9 +413,9 @@ public class CustomContentProviderUtilsTest {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(1000L);
statistics.setStopTime(2500L);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(1000L);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
@@ -454,10 +423,8 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0);
track.setTripStatistics(statistics);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
@@ -546,7 +513,7 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(Location[], int, long)}.
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], int, long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
@@ -555,9 +522,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), -1, trackId);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), -1, trackId);
Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), 8, trackId);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), 8, trackId);
Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
@@ -613,7 +580,7 @@ public class CustomContentProviderUtilsTest {
byte[] sensor = "Sensor state".getBytes();
when(cursorMock.getBlob(index++)).thenReturn(sensor);
Location location = contentProviderUtils.createTrackPoint(cursorMock);
TrackPoint location = contentProviderUtils.createTrackPoint(cursorMock);
Assert.assertEquals(longitude, location.getLongitude(), 0.01);
Assert.assertEquals(latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01);
@@ -622,7 +589,7 @@ public class CustomContentProviderUtilsTest {
/**
* Tests the method
* {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
* {@link ContentProviderUtils#insertTrackPoint(TrackPoint, long)}.
*/
@Test
public void testInsertTrackPoint() {
@@ -631,7 +598,7 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
contentProviderUtils.insertTrackPoint(TestDataUtil.createLocation(22), trackId);
contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId);
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
@@ -645,8 +612,8 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Location lastLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastLocation);
TrackPoint lastTrackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastTrackPoint.getLocation());
}
/**
@@ -659,9 +626,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getLocations().size()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
@@ -678,9 +645,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getLocations().size()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
@@ -688,7 +655,7 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
@@ -697,25 +664,25 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getLocations().size()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[9];
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(startTrackPointId - i, locationIterator.getTrackPointId());
checkLocation((trackpointIds.length - 1) - i, location);
Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
Assert.assertEquals(startTrackPointId - i, trackPointIterator.getTrackPointId());
checkLocation((trackpointIds.length - 1) - i, trackPoint.getLocation());
}
Assert.assertFalse(locationIterator.hasNext());
Assert.assertFalse(trackPointIterator.hasNext());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
@@ -724,20 +691,20 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getLocations().size()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[0];
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
TrackPoint trackPoint = locationIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId());
checkLocation(i, location);
checkLocation(i, trackPoint.getLocation());
}
Assert.assertFalse(locationIterator.hasNext());
}
@@ -762,7 +729,7 @@ public class CustomContentProviderUtilsTest {
*/
private void insertTrackWithLocations(Track track) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
}
@Test
@@ -29,7 +29,7 @@ public class CustomSQLiteOpenHelperTest {
private static final String DATABASE_NAME = "test.db";
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Get the SQL create statements for all tables (ordered by name).
@@ -30,6 +30,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -63,287 +64,269 @@ public class ChartFragmentTest {
boolean chartByDistance = false;
chartFragment = (ChartFragment) ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
chartFragment.setTrackStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
/**
* 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#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_sensorIncorrect() {
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_sensorIncorrect() {
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Input incorrect state.
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE);
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the correct values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_sensorCorrect() {
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_sensorCorrect() {
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102);
// Creates SensorDataSet.
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1], 0.01);
point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(100.0, point.getHeartRate(), 0.01);
Assert.assertEquals(101.0, point.getCadence(), 0.01);
Assert.assertEquals(102.0, point.getPower(), 0.01);
}
/**
* Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of metric Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_distanceMetric() {
// By distance.
public void testCreatePendingPoint_distanceMetric() {
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second is a same location, just different time.
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use metric.
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint3);
// Computes the distance between Latitude 22 and 23.
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(),
trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals(distance1, point[0], 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The fourth location is a new location, and use metric.
TrackPoint sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint();
trackPoint4.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint4);
// Computes the distance between Latitude 23 and 24.
Location.distanceBetween(sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(),
sensorDataSetLocation4.getLatitude(), sensorDataSetLocation4.getLongitude(), results);
Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(),
trackPoint4.getLatitude(), trackPoint4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals((distance1 + distance2), point[0], 0.01);
Assert.assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_distanceImperial() {
public void testCreatePendingPoint_distanceImperial() {
// By distance.
chartFragment.setChartByDistance(true);
// Setups to use imperial.
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second location is a new location, and use imperial.
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation2);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint2);
/*
* Computes the distance between Latitude 22 and 23.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0], 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use imperial.
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation3);
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint3);
/*
* Computes the distance between Latitude 23 and 24.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
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.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the values of time in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_time() {
// By time
public void testCreatePendingPoint_time() {
chartFragment.setChartByDistance(false);
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
long timeSpan = 222;
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setTime(trackPoint1.getTime() + timeSpan);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location, double[])} by one and two points.
* Tests the logic to get the value of elevation in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
*/
@Test
public void testFillDataPoint_elevation() {
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_elevation() {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
/*
* At first, clear old points of elevation, so give true to the second parameter.
* Then only one value INITIAL_ALTITUDE in buffer.
*/
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
point[ChartView.ELEVATION_SERIES + 1], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
}
/**
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of speed in {@link ChartFragment#createPendingPoint(TrackPoint)}.
* In this test, firstly remove all points in memory, and then fill in two points one by one.
* The speed values of these points are 129, 130.
*/
@Test
public void testFillDataPoint_speed() {
public void testCreatePendingPoint_speed() {
/*
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(128.5f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
/*
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint1.getTime() + 222);
trackPoint2.setSpeed(130);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point.getSpeed(), 0.01);
}
/**
* Tests the logic to compute speed when use Imperial.
*/
@Test
public void testFillDataPoint_speedImperial() {
// Setups to use imperial.
public void testCreatePendingPoint_speedImperial() {
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false.
*/
@Test
public void testFillDataPoint_pace_nonZeroSpeed() {
// Setups reportSpeed to false.
public void testCreatePendingPoint_pace_nonZeroSpeed() {
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point.getPace(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false and average speed is zero.
*/
@Test
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false);
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01);
}
/**
* Helper method to test fillDataPoint.
*
* @param location location to fill
* @return data of this location
*/
private double[] fillDataPointTestHelper(Location location) {
double[] point = new double[ChartView.NUM_SERIES + 1];
chartFragment.fillDataPoint(location, point);
return point;
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(0.0, point.getPace(), 0.01);
}
}
@@ -33,6 +33,7 @@ import java.util.Locale;
import java.util.SimpleTimeZone;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
@@ -83,24 +84,24 @@ public abstract class AbstractTestFileTrackImporter {
@Mock
public ContentProviderUtils contentProviderUtils;
Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index);
location.setAltitude(TRACK_ELEVATION + index);
location.setTime(time);
return location;
TrackPoint createTrackPoint(int index, long time) {
Location trackPoint = new Location(LocationManager.GPS_PROVIDER);
trackPoint.setLatitude(TRACK_LATITUDE + index);
trackPoint.setLongitude(TRACK_LONGITUDE + index);
trackPoint.setAltitude(TRACK_ELEVATION + index);
trackPoint.setTime(time);
return new TrackPoint(trackPoint);
}
/**
* Expects the first track point to be added.
*
* @param location the location
* @param trackPoint the trackPoint
* @param trackId the track id
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(location != null ? (Location[]) any() : (Location[]) any(), eq(1), eq(trackId))).thenReturn(1);
protected void expectFirstTrackPoint(TrackPoint trackPoint, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(1), eq(trackId))).thenReturn(1);
}
/**
@@ -124,7 +125,7 @@ public abstract class AbstractTestFileTrackImporter {
Assert.assertEquals(name, track.getName());
Assert.assertEquals(description, track.getDescription());
if (time != -1L) {
Assert.assertEquals(time, track.getTripStatistics().getStartTime());
Assert.assertEquals(time, track.getTrackStatistics().getStartTime_ms());
}
}
}
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
@@ -21,10 +20,12 @@ import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -32,23 +33,23 @@ import static org.junit.Assert.assertNotNull;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* TODO: test ignores {@link de.dennisguse.opentracks.stats.TripStatistics} for now.
* TODO: test ignores {@link TrackStatistics} for now.
*/
@RunWith(JUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private static final String TRACK_ICON = "the track icon";
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Waypoint> waypoints = new ArrayList<>();
private long importTrackId;
private long trackId = System.currentTimeMillis();
private final long trackId = System.currentTimeMillis();
@Before
public void setUp() {
@@ -57,17 +58,16 @@ public class ExportImportTest {
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint();
Waypoint waypoint = new Waypoint(track.getTrackPoints().get(i).getLocation());
waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i);
waypoint.setIcon("the waypoing icon" + i);
waypoint.setPhotoUrl("the photo url" + i);
waypoint.setTrackId(trackId);
waypoint.setLocation(track.getLocations().get(i));
contentProviderUtils.insertWaypoint(waypoint);
waypoints.add(waypoint);
@@ -114,7 +114,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getLocations(), importedTrack.getLocations());
assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -184,7 +184,7 @@ public class ExportImportTest {
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
assertEquals(track.getLocations(), trackImported.getLocations());
assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,6 +26,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -74,9 +73,9 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
String longitude = Double.toString(TRACK_LONGITUDE + index);
String elevation = Double.toString(TRACK_ELEVATION + index);
StringBuilder buffer = new StringBuilder();
buffer.append("<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
buffer.append("<trkpt lat=\"").append(latitude).append("\" lon=\"").append(longitude).append("\"><ele>").append(elevation).append("</ele>");
if (time != null) {
buffer.append("<time>" + time + "</time>");
buffer.append("<time>").append(time).append("</time>");
}
buffer.append("</trkpt>");
return buffer.toString();
@@ -85,14 +84,14 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test
public void testOneTrackOneSegment() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint1 = createTrackPoint(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -109,19 +108,19 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
@Test
public void testOneTrackTwoSegments() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -141,7 +140,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
@@ -153,7 +152,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -169,7 +168,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
}
@@ -207,7 +206,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,6 +26,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -69,14 +68,14 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test
public void testOneTrackOneSegment() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPointl1 = createTrackPoint(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -94,20 +93,20 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
@Test
public void testOneTrackTwoSegments() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -127,7 +126,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services;
import android.app.NotificationManager;
import android.content.Context;
import android.location.Location;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
@@ -12,6 +11,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
@@ -21,10 +22,10 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private Location locationMock;
private TrackPoint locationMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@@ -42,10 +43,10 @@ public class TrackRecordingServiceNotificationManagerTest {
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
// when
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 1000);
subject.updateLocation(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 1000);
subject.updateTrackPoint(context, locationMock, 100);
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
@@ -46,7 +46,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -66,7 +66,7 @@ public class TrackRecordingServiceTest {
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private final long trackId = Math.abs(new Random().nextLong());
@@ -193,9 +193,9 @@ public class TrackRecordingServiceTest {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
TripStatistics tripStatistics = new TripStatistics();
tripStatistics.setStopTime(stopTime);
dummyTrack.setTripStatistics(tripStatistics);
TrackStatistics trackStatistics = new TrackStatistics();
trackStatistics.setStopTime_ms(stopTime);
dummyTrack.setTrackStatistics(trackStatistics);
addTrack(dummyTrack, isRecording);
}
@@ -218,7 +218,7 @@ public class TrackRecordingServiceTest {
location.setSpeed(10);
location.setTime(System.currentTimeMillis());
location.setBearing(3.0f);
trackRecordingService.insertTrackPoint(location);
trackRecordingService.insertLocation(location);
Thread.sleep(200);
}
@@ -28,7 +28,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -45,7 +45,7 @@ public class TrackRecordingServiceTestLooper {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -215,7 +215,7 @@ public class TrackRecordingServiceTestLooper {
location.setSpeed(10);
location.setTime(startTime + i * 10000);
location.setBearing(3.0f);
service.insertTrackPoint(location);
service.insertLocation(location);
if (i % 7 == 0) {
service.insertWaypoint(null, null, null, null);
@@ -229,9 +229,9 @@ public class TrackRecordingServiceTestLooper {
track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
Assert.assertNotNull(tripStatistics);
Assert.assertTrue(tripStatistics.getStartTime() > 0);
Assert.assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime());
TrackStatistics trackStatistics = track.getTrackStatistics();
Assert.assertNotNull(trackStatistics);
Assert.assertTrue(trackStatistics.getStartTime_ms() > 0);
Assert.assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
}
}
@@ -36,7 +36,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
@@ -22,28 +22,28 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TripStatistics}.
* Tests for {@link TrackStatistics}.
* This only tests non-trivial pieces of that class.
*
* @author Rodrigo Damazio
*/
@RunWith(JUnit4.class)
public class TripStatisticsTest {
public class TrackStatisticsTest {
private TripStatistics statistics;
private TrackStatistics statistics;
@Before
public void setUp() {
statistics = new TripStatistics();
statistics = new TrackStatistics();
}
@Test
public void testMerge() {
TripStatistics statistics2 = new TripStatistics();
statistics.setStartTime(1000L); // Resulting start time
statistics.setStopTime(2500L);
statistics2.setStartTime(3000L);
statistics2.setStopTime(4000L); // Resulting stop time
TrackStatistics statistics2 = new TrackStatistics();
statistics.setStartTime_ms(1000L); // Resulting start time
statistics.setStopTime_ms(2500L);
statistics2.setStartTime_ms(3000L);
statistics2.setStopTime_ms(4000L); // Resulting stop time
statistics.setTotalTime(1500L);
statistics2.setTotalTime(1000L); // Result: 1500+1000
statistics.setMovingTime(700L);
@@ -58,15 +58,11 @@ public class TripStatisticsTest {
statistics.setMinElevation(1200.0); // Resulting min elevation
statistics2.setMaxElevation(3575.0); // Resulting max elevation
statistics2.setMinElevation(2800.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0); // Resulting min grade
statistics2.setMaxGrade(35.0); // Resulting max grade
statistics2.setMinGrade(0.0);
statistics.merge(statistics2);
Assert.assertEquals(1000L, statistics.getStartTime());
Assert.assertEquals(4000L, statistics.getStopTime());
Assert.assertEquals(1000L, statistics.getStartTime_ms());
Assert.assertEquals(4000L, statistics.getStopTime_ms());
Assert.assertEquals(2500L, statistics.getTotalTime());
Assert.assertEquals(1300L, statistics.getMovingTime());
Assert.assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
@@ -74,8 +70,6 @@ public class TripStatisticsTest {
Assert.assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
Assert.assertEquals(1200.0, statistics.getMinElevation(), 0.001);
Assert.assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
Assert.assertEquals(-25.0, statistics.getMinGrade(), 0.001);
Assert.assertEquals(35.0, statistics.getMaxGrade(), 0.001);
}
@Test
@@ -9,16 +9,16 @@ import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class)
public class AnnouncementUtilsTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void getAnnouncement() {
TripStatistics stats = new TripStatistics();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
@@ -21,6 +21,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
import java.util.Arrays;
/**
* Tests for {@link FileUtils}.
@@ -108,9 +109,7 @@ public class FileUtilsTest {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
char[] name = new char[FileUtils.MAX_FAT32_PATH_LENGTH];
for (int i = 0; i < name.length; i++) {
name[i] = 'a';
}
Arrays.fill(name, 'a');
String nameString = new String(name);
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
@@ -36,7 +36,7 @@ import java.util.TimeZone;
@RunWith(AndroidJUnit4.class)
public class StringUtilsTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
/**
+1 -3
View File
@@ -15,9 +15,7 @@ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3305"
android:versionName="v3.5.0"> <!-- For FDroid. -->
android:installLocation="auto">
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -26,7 +26,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
*
* @author Jimmy Shih
*/
public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final long[] trackIds;
private final Context context;
@@ -88,7 +88,7 @@ public class MarkerEditActivity extends AbstractActivity {
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
@@ -254,8 +254,8 @@ public class MarkerEditActivity extends AbstractActivity {
private void hideAndShowOptions() {
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
insertPhotoMenuItem.setVisible(isPhotoSet ? false : true);
insertGalleryImgMenuItem.setVisible(isPhotoSet ? false : true);
insertPhotoMenuItem.setVisible(!isPhotoSet);
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
}
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
}
@@ -50,7 +50,7 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
@@ -172,7 +172,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
searchEngine = new SearchEngine(contentProviderUtils);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@@ -424,7 +424,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private Map<String, Object> prepareTrackForDisplay(Track track) {
Map<String, Object> resultMap = new HashMap<>();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
@@ -433,10 +433,10 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatistics.getTotalDistance(), metricUnits));
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, trackStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatistics.getStartTime());
resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
@@ -21,11 +21,13 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@@ -43,6 +45,7 @@ import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -50,7 +53,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
* An activity to show the track detail.
* An activity to show the track detail, record a new track or resumes an existing one.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
@@ -89,6 +92,34 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
trackController.update(isRecording(), recordingTrackPaused);
}
});
if (recordingTrackId == -1L) {
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
return;
}
// Starts or resumes a track.
int msg;
if (trackId == -1L) {
// trackId isn't initialized -> leads a new recording.
trackId = service.startNewTrack();
recordingTrackId = trackId;
msg = R.string.track_detail_record_success;
} else {
// trackId is initialized -> resumes the track.
recordingTrackId = trackId;
service.resumeTrack(trackId);
msg = R.string.track_detail_resume_success;
}
// A recording track is on.
Toast.makeText(TrackDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
trackDataHub.loadTrack(trackId);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
}
}
};
@@ -131,6 +162,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private MenuItem insertMarkerMenuItem;
private MenuItem markerListMenuItem;
private MenuItem shareMenuItem;
private MenuItem resumeMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
@@ -167,7 +199,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackDataHub = new TrackDataHub(this);
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@@ -258,11 +290,16 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
// Update UI
this.invalidateOptionsMenu();
trackController.onResume(isRecording(), recordingTrackPaused);
if (trackId != -1L) {
trackDataHub.loadTrack(trackId);
trackController.onResume(isRecording(), recordingTrackPaused);
} else {
startRecording();
}
}
@Override
@@ -304,6 +341,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
shareMenuItem = menu.findItem(R.id.track_detail_share);
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
resumeMenuItem = menu.findItem(R.id.track_detail_resume_track);
return super.onCreateOptionsMenu(menu);
}
@@ -345,6 +383,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
case R.id.track_detail_delete:
deleteTracks(new long[]{trackId});
return true;
case R.id.track_detail_resume_track:
startRecording();
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
@@ -387,9 +428,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return;
}
trackId = waypoint.getTrackId();
if (trackId == -1L) {
finish();
return;
}
}
if (trackId == -1L) {
finish();
return;
}
Track track = contentProviderUtils.getTrack(trackId);
@@ -416,9 +461,12 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
String title;
if (isRecording()) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
resumeMenuItem.setVisible(false);
} else {
Track track = contentProviderUtils.getTrack(trackId);
title = track != null ? track.getName() : "";
// Only visible if there aren't other tracks that is recording.
resumeMenuItem.setVisible(recordingTrackId == -1L);
}
setTitle(title);
}
@@ -437,4 +485,15 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private boolean isRecording() {
return trackId == recordingTrackId;
}
private void startRecording() {
trackRecordingServiceConnection.startAndBind(this);
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
}
}
@@ -65,7 +65,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.e(TAG, "invalid trackId");
@@ -160,7 +160,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
// private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
@@ -172,8 +171,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
@@ -188,7 +185,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
});
if (!startGps && !startNewRecording) {
if (!startGps) {
return;
}
@@ -197,17 +194,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
Log.d(TAG, "service not available to start gps or a new recording");
return;
}
if (startNewRecording) {
startGps = false;
long trackId = service.startNewTrack();
startNewRecording = false;
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
Toast.makeText(TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show();
}
if (startGps) {
service.startGps();
startGps = false;
@@ -220,18 +206,19 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
startRecording();
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
@@ -244,7 +231,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
@@ -456,7 +443,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
/**
* Shows start up dialogs.
*/
public void showStartupDialogs() {
private void showStartupDialogs() {
// If stats_units_key is undefined, set it
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
@@ -479,7 +466,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
boolean hasTrack = listView != null && listView.getCount() != 0;
if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) {
@@ -489,21 +475,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
}
/**
* Starts a new recording.
*/
private void startRecording() {
startNewRecording = true;
trackRecordingServiceConnection.startAndBind(this);
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
}
/**
* Handles a context item selection.
*
@@ -0,0 +1,103 @@
package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions;
public class ChartPoint {
//X-axis
private double timeOrDistance;
//Y-axis
private double elevation;
private double speed;
private double pace;
private double heartRate = Double.NaN;
private double cadence = Double.NaN;
private double power = Double.NaN;
@VisibleForTesting
ChartPoint(double elevation) {
this.elevation = elevation;
}
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = trackStatistics.getTotalTime();
}
elevation = trackStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
}
}
public double getTimeOrDistance() {
return timeOrDistance;
}
public double getElevation() {
return elevation;
}
public double getSpeed() {
return speed;
}
public double getPace() {
return pace;
}
public boolean hasHeartRate() {
return Double.isNaN(heartRate);
}
public double getHeartRate() {
return heartRate;
}
public boolean hasCadence() {
return Double.isNaN(cadence);
}
public double getCadence() {
return cadence;
}
public boolean hasPower() {
return Double.isNaN(power);
}
public double getPower() {
return power;
}
}
@@ -23,6 +23,8 @@ import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.NonNull;
import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
@@ -32,7 +34,7 @@ import de.dennisguse.opentracks.stats.ExtremityMonitor;
*
* @author Sandor Dornbush
*/
class ChartValueSeries {
abstract class ChartValueSeries {
private static final float STROKE_WIDTH = 2f;
@@ -126,26 +128,26 @@ class ChartValueSeries {
}
/**
* Updates the series with a new value.
*
* @param value the new value
* Updates the series with a new {@link ChartPoint}.
*/
void update(double value) {
extremityMonitor.update(value);
void update(ChartPoint chartPoint) {
if (isChartPointValid(chartPoint)) {
extremityMonitor.update(extractDataFromChartPoint(chartPoint));
}
}
/**
* Gets the path.
*/
abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
boolean isChartPointValid(@NonNull ChartPoint chartPoint) {
return !Double.isNaN(extractDataFromChartPoint(chartPoint));
}
protected abstract boolean drawIfChartPointHasNoData();
Path getPath() {
return path;
}
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
@@ -33,6 +33,7 @@ import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
@@ -62,14 +63,6 @@ public class ChartView extends View {
static final int Y_AXIS_INTERVALS = 5;
public static final int NUM_SERIES = 6;
public static final int ELEVATION_SERIES = 0;
public static final int SPEED_SERIES = 1;
public static final int PACE_SERIES = 2;
public static final int HEART_RATE_SERIES = 3;
public static final int CADENCE_SERIES = 4;
public static final int POWER_SERIES = 5;
private static final int TARGET_X_AXIS_INTERVALS = 4;
private static final int MIN_ZOOM_LEVEL = 1;
@@ -89,8 +82,11 @@ public class ChartView extends View {
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final List<double[]> chartData = new ArrayList<>();
private final List<ChartValueSeries> seriesList = new ArrayList<>();
private final ChartValueSeries speedSeries;
private final ChartValueSeries paceSeries;
private final List<ChartPoint> chartPoints = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final Paint axisPaint;
@@ -192,58 +188,124 @@ public class ChartView extends View {
}
});
public ChartView(Context context, boolean chartByDistance) {
public ChartView(Context context, final boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
series[ELEVATION_SERIES] = new ChartValueSeries(context,
seriesList.add(new ChartValueSeries(context,
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border);
series[SPEED_SERIES] = new ChartValueSeries(context,
R.color.chart_elevation_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return true;
}
});
speedSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 5, 10, 20, 50, 100},
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.color.chart_speed_fill,
R.color.chart_speed_border);
series[PACE_SERIES] = new ChartValueSeries(context,
R.color.chart_speed_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return reportSpeed;
}
};
seriesList.add(speedSeries);
paceSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.color.chart_pace_fill,
R.color.chart_pace_border);
series[HEART_RATE_SERIES] = new ChartValueSeries(context,
R.color.chart_pace_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return !reportSpeed;
}
};
seriesList.add(paceSeries);
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{25, 50},
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border);
series[CADENCE_SERIES] = new ChartValueSeries(context,
R.color.chart_heart_rate_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50},
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border);
series[POWER_SERIES] = new ChartValueSeries(context,
R.color.chart_cadence_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
1000,
new int[]{5, 50, 100, 200},
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border);
R.color.chart_power_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
float scale = context.getResources().getDisplayMetrics().density;
@@ -279,6 +341,10 @@ public class ChartView extends View {
setFocusable(true);
setClickable(true);
updateDimensions();
// either speedSeries or paceSeries should be enabled.
speedSeries.setEnabled(reportSpeed);
paceSeries.setEnabled(!reportSpeed);
}
@Override
@@ -286,15 +352,6 @@ public class ChartView extends View {
return true;
}
/**
* Sets the enabled value for a chart value series.
*
* @param index the chart value series index
*/
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
}
public boolean getMetricUnits() {
return metricUnits;
}
@@ -315,35 +372,41 @@ public class ChartView extends View {
/**
* Sets report speed.
*
* @param value true to report speed
* @param value report speed (true) or pace (false)
*/
public void setReportSpeed(boolean value) {
reportSpeed = value;
}
/**
* Sets show pointer.
*
* @param value true to show pointer
*/
public boolean applyReportSpeed() {
if (reportSpeed) {
if (!speedSeries.isEnabled()) {
speedSeries.setEnabled(true);
paceSeries.setEnabled(false);
return true;
}
} else {
if (!paceSeries.isEnabled()) {
speedSeries.setEnabled(false);
paceSeries.setEnabled(true);
return true;
}
}
return false;
}
public void setShowPointer(boolean value) {
showPointer = value;
}
/**
* Adds data points.
*
* @param dataPoints an array of data points to be added
*/
public void addDataPoints(List<double[]> dataPoints) {
synchronized (chartData) {
chartData.addAll(dataPoints);
for (double[] dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint[0]);
for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) {
series[j].update(dataPoint[j + 1]);
}
public void addChartPoints(List<ChartPoint> dataPoints) {
synchronized (chartPoints) {
chartPoints.addAll(dataPoints);
for (ChartPoint dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint.getTimeOrDistance());
for (ChartValueSeries i : seriesList) {
i.update(dataPoint);
}
}
updateDimensions();
@@ -355,8 +418,8 @@ public class ChartView extends View {
* Clears all data.
*/
public void reset() {
synchronized (chartData) {
chartData.clear();
synchronized (chartPoints) {
chartPoints.clear();
xExtremityMonitor.reset();
zoomLevel = 1;
updateDimensions();
@@ -364,50 +427,34 @@ public class ChartView extends View {
}
/**
* Resets scroll. To be called on the UI thread.
* Resets scroll.
* To be called on the UI thread.
*/
public void resetScroll() {
scrollTo(0, 0);
}
/**
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Returns true if can zoom in.
*/
public boolean canZoomIn() {
private boolean canZoomIn() {
return zoomLevel < MAX_ZOOM_LEVEL;
}
/**
* Returns true if can zoom out.
*/
public boolean canZoomOut() {
private boolean canZoomOut() {
return zoomLevel > MIN_ZOOM_LEVEL;
}
/**
* Zooms in one level.
*/
public void zoomIn() {
private void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
updatePaths();
@@ -415,10 +462,7 @@ public class ChartView extends View {
}
}
/**
* Zooms out one level.
*/
public void zoomOut() {
private void zoomOut() {
if (canZoomOut()) {
zoomLevel--;
scroller.abortAnimation();
@@ -438,7 +482,7 @@ public class ChartView extends View {
*
* @param velocityX velocity of fling in pixels per second
*/
public void fling(int velocityX) {
private void fling(int velocityX) {
int maxWidth = effectiveWidth * (zoomLevel - 1);
scroller.fling(getScrollX(), 0, velocityX, 0, 0, maxWidth, 0, 0);
invalidate();
@@ -449,7 +493,7 @@ public class ChartView extends View {
*
* @param deltaX the number of pixels to scroll
*/
public void scrollBy(int deltaX) {
private void scrollBy(int deltaX) {
int scrollX = getScrollX() + deltaX;
if (scrollX < 0) {
scrollX = 0;
@@ -493,7 +537,7 @@ public class ChartView extends View {
@Override
protected void onDraw(Canvas canvas) {
synchronized (chartData) {
synchronized (chartPoints) {
canvas.save();
canvas.drawColor(Color.WHITE);
@@ -536,7 +580,7 @@ public class ChartView extends View {
* @param canvas the canvas
*/
private void drawDataSeries(Canvas canvas) {
for (ChartValueSeries chartValueSeries : series) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
chartValueSeries.drawPath(canvas);
}
@@ -550,11 +594,7 @@ public class ChartView extends View {
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
for (Waypoint waypoint : waypoints) {
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) {
continue;
@@ -602,9 +642,8 @@ public class ChartView extends View {
int lines = titleDimensions[0];
int lineHeight = titleDimensions[1];
int count = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
Paint paint = chartValueSeries.getTitlePaint();
@@ -622,9 +661,8 @@ public class ChartView extends View {
private int[] getTitleDimensions() {
int lines = 0;
int lineHeight = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
@@ -658,9 +696,6 @@ public class ChartView extends View {
}
}
/**
* Gets the x axis label.
*/
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
@@ -684,9 +719,6 @@ public class ChartView extends View {
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
}
/**
* Gets the x axis interval.
*/
private double getXAxisInterval() {
double interval = maxX / zoomLevel / TARGET_X_AXIS_INTERVALS;
if (interval < 1) {
@@ -701,16 +733,13 @@ public class ChartView extends View {
return interval;
}
/**
* Gets the x axis marker positions.
*/
private List<Double> getXAxisMarkerPositions(double interval) {
List<Double> markers = new ArrayList<>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
}
// At least 2 markers
if (markers.size() < 2) {
markers.add(maxX);
}
@@ -727,11 +756,12 @@ public class ChartView extends View {
int y = topBorder;
canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint);
//TODO
int markerXPosition = x - spacer;
for (int i = 0; i < series.length; i++) {
int index = series.length - 1 - i;
ChartValueSeries chartValueSeries = series[index];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(index)) {
for (int i = 0; i < seriesList.size(); i++) {
int index = seriesList.size() - 1 - i;
ChartValueSeries chartValueSeries = seriesList.get(index);
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer;
}
}
@@ -779,29 +809,29 @@ public class ChartView extends View {
* @param canvas the canvas
*/
private void drawPointer(Canvas canvas) {
int index = -1;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
ChartValueSeries firstChartValueSeries = null;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
index = i;
firstChartValueSeries = chartValueSeries;
break;
}
}
if (index != -1 && chartData.size() > 0) {
if (firstChartValueSeries != null && chartPoints.size() > 0) {
int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2;
int dy = getY(series[index], chartData.get(chartData.size() - 1)[index + 1])
- pointer.getIntrinsicHeight();
double value = firstChartValueSeries.extractDataFromChartPoint(chartPoints.get(chartPoints.size() - 1));
int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight();
canvas.translate(dx, dy);
pointer.draw(canvas);
}
}
/**
* Updates paths. The path needs to be updated any time after the data or the dimensions change.
* Updates paths.
* The path needs to be updated any time after the data or the dimensions change.
*/
private void updatePaths() {
synchronized (chartData) {
for (ChartValueSeries chartValueSeries : series) {
synchronized (chartPoints) {
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.getPath().reset();
}
drawPaths();
@@ -809,24 +839,23 @@ public class ChartView extends View {
}
}
/**
* Draws all paths.
*/
private void drawPaths() {
boolean[] hasMoved = new boolean[series.length];
boolean[] hasMoved = new boolean[seriesList.size()];
for (double[] dataPoint : chartData) {
for (int j = 0; j < series.length; j++) {
double value = dataPoint[j + 1];
if (Double.isNaN(value)) {
for (ChartPoint dataPoint : chartPoints) {
for (int i = 0; i < seriesList.size(); i++) {
ChartValueSeries chartValueSeries = seriesList.get(i);
if (!chartValueSeries.isChartPointValid(dataPoint)) {
continue;
}
ChartValueSeries chartValueSeries = series[j];
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
Path path = chartValueSeries.getPath();
int x = getX(dataPoint[0]);
int x = getX(dataPoint.getTimeOrDistance());
int y = getY(chartValueSeries, value);
if (!hasMoved[j]) {
hasMoved[j] = true;
if (!hasMoved[i]) {
hasMoved[i] = true;
path.moveTo(x, y);
} else {
path.lineTo(x, y);
@@ -835,37 +864,32 @@ public class ChartView extends View {
}
}
/**
* Closes all paths.
*/
private void closePaths() {
for (int i = 0; i < series.length; i++) {
int first = getFirstPopulatedChartDataIndex(i);
for (ChartValueSeries chartValueSeries : seriesList) {
int first = getFirstPopulatedChartDataIndex(chartValueSeries);
if (first != -1) {
int xCorner = getX(chartData.get(first)[0]);
int xCorner = getX(chartPoints.get(first).getTimeOrDistance());
int yCorner = topBorder + effectiveHeight;
ChartValueSeries chartValueSeries = series[i];
Path path = chartValueSeries.getPath();
// Bottom right corner
path.lineTo(getX(chartData.get(chartData.size() - 1)[0]), yCorner);
path.lineTo(getX(chartPoints.get(chartPoints.size() - 1).getTimeOrDistance()), yCorner);
// Bottom left corner
path.lineTo(xCorner, yCorner);
// Top right corner
path.lineTo(xCorner, getY(chartValueSeries, chartData.get(first)[i + 1]));
double value = chartValueSeries.extractDataFromChartPoint(chartPoints.get(first));
path.lineTo(xCorner, getY(chartValueSeries, value));
}
}
}
/**
* Finds the index of the first data point containing data for a series.
* Returns -1 if no data point contains data for the series.
*
* @param seriesIndex the series's index
* @return -1 if no data point contains data for the series.
*/
private int getFirstPopulatedChartDataIndex(int seriesIndex) {
for (int i = 0; i < chartData.size(); i++) {
if (!Double.isNaN(chartData.get(i)[seriesIndex + 1])) {
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
for (int i = 0; i < chartPoints.size(); i++) {
if (chartValueSeries.isChartPointValid(chartPoints.get(i))) {
return i;
}
}
@@ -877,7 +901,7 @@ public class ChartView extends View {
*/
private void updateDimensions() {
maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0;
for (ChartValueSeries chartValueSeries : series) {
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.updateDimension();
}
float density = getContext().getResources().getDisplayMetrics().density;
@@ -885,9 +909,8 @@ public class ChartView extends View {
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
int markerLength = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker());
markerLength += rect.width() + spacer;
}
@@ -980,22 +1003,12 @@ public class ChartView extends View {
/**
* Returns true if the index is allowed when the chartData is empty.
*
* @param index the index
*/
private boolean allowIfEmpty(int index) {
if (!chartData.isEmpty()) {
private boolean allowIfEmpty(ChartValueSeries chartValueSeries) {
if (!chartPoints.isEmpty()) {
return false;
}
switch (index) {
case ELEVATION_SERIES:
return true;
case SPEED_SERIES:
return reportSpeed;
case PACE_SERIES:
return !reportSpeed;
default:
return false;
}
return chartValueSeries.drawIfChartPointHasNoData();
}
}
@@ -1,44 +0,0 @@
/*
* Copyright 2011 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.content;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
/**
* Data source on the phone.
*
* @author Rodrigo Damazio
*/
class DataSource {
private final ContentResolver contentResolver;
DataSource(Context context) {
contentResolver = context.getContentResolver();
}
void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer);
}
void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
}
@@ -1,41 +0,0 @@
/*
* Copyright 2011 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.content;
/**
* Listener to be invoked when {@link DataSource} changes.
*
* @author Jimmy Shih
*/
public interface DataSourceListener {
/**
* Notifies when the tracks table is updated.
*/
void notifyTracksTableUpdated();
/**
* Notifies when the waypoints table is updated.
*/
void notifyWaypointsTableUpdated();
/**
* Notifies when the track points table is updated.
*/
void notifyTrackPointsTableUpdated();
}
@@ -1,207 +0,0 @@
/*
* Copyright 2011 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.content;
import android.database.ContentObserver;
import android.os.Handler;
import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Creates observers/listeners and manages their registration with {@link DataSource}.
* The observers/listeners calls {@link DataSourceListener} when data changes.
*
* @author Rodrigo Damazio
*/
class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
handler = new Handler();
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
}
/**
* Updates listeners with data source.
*
* @param listeners the listeners
*/
void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
// Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to the same internal listener
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
}
Log.d(TAG, "Updating listeners " + neededListeners);
// Unnecessary = registered - needed
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove unnecessary listeners
for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(trackDataType);
}
// Add missing listeners
for (TrackDataType trackDataType : missingListeners) {
registerListener(trackDataType);
}
// Update registered listeners
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
}
/**
* Registers a listener with data source.
*
* @param trackDataType the listener data type
*/
private void registerListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
default:
break;
}
}
/**
* Unregisters a listener with data source.
*
* @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
default:
break;
}
}
/**
* Unregisters all listeners with data source.
*/
void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType);
}
}
/**
* Observer when the tracks table is updated.
*
* @author Jimmy Shih
*/
private class TracksTableObserver extends ContentObserver {
TracksTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTracksTableUpdated();
}
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
}
@@ -23,7 +23,7 @@ import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -67,7 +67,7 @@ public class DescriptionGenerator {
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
builder.append(generateTrackStatisticsDescription(track.getTrackStatistics(), html));
return builder.toString();
}
@@ -89,12 +89,12 @@ public class DescriptionGenerator {
}
/**
* Generates a description for a {@link TripStatistics}.
* Generates a description for a {@link TrackStatistics}.
*
* @param stats the trip statistics
* @param stats the track statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
private String generateTrackStatisticsDescription(TrackStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
@@ -134,14 +134,8 @@ public class DescriptionGenerator {
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
builder.append(lineBreak);
return builder.toString();
@@ -228,19 +222,4 @@ public class DescriptionGenerator {
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
}
@@ -33,7 +33,7 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -247,9 +247,9 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
TrackStatistics statistics = track.getTrackStatistics();
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
long meanTimestamp = (statistics.getStartTime_ms() + statistics.getStopTime_ms()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
@@ -16,10 +16,11 @@
package de.dennisguse.opentracks.content;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
@@ -27,24 +28,29 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Track data hub. Receives data from {@link DataSource} and distributes it to {@link TrackDataListener} after some processing.
* Track data hub.
* Receives data from {@link de.dennisguse.opentracks.content.provider.CustomContentProvider} and distributes it to {@link TrackDataListener} after some processing.
*
* {@link TrackPoint}s are filtered/downsampled with a dynamic sampling frequency.
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSharedPreferenceChangeListener {
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
/**
* Target number of track points displayed by the map overlay.
@@ -70,8 +76,6 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private DataSource dataSource;
private DataSourceManager dataSourceManager;
// Preference values
private long selectedTrackId;
@@ -80,8 +84,13 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
// Track points sampling state
private int numLoadedPoints;
private long firstSeenLocationId;
private long lastSeenLocationId;
private long firstSeenTrackPointId;
private long lastSeenTrackPointId;
// Registered listeners
private ContentObserver tracksTableObserver;
private ContentObserver waypointsTableObserver;
private ContentObserver trackPointsTableObserver;
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
@@ -102,19 +111,43 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
return;
}
started = true;
handlerThread = new HandlerThread("TrackDataHubHandlerThread");
handlerThread = new HandlerThread(TAG);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
dataSource = newDataSource();
dataSourceManager = new DataSourceManager(dataSource, this);
//register listeners
ContentResolver contentResolver = context.getContentResolver();
tracksTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
}
};
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
waypointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyWaypointsTableUpdate(trackDataManager.getListenerWaypoints());
}
};
contentResolver.registerContentObserver(WaypointsColumns.CONTENT_URI, false, waypointsTableObserver);
trackPointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
PreferencesUtils.register(context, this);
onSharedPreferenceChanged(null, null);
runInHandlerThread(new Runnable() {
@Override
public void run() {
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
if (started) {
loadDataForAll();
}
}
@@ -131,14 +164,17 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
started = false;
dataSourceManager.unregisterAllListeners();
//Unregister listeners
ContentResolver contentResolver = context.getContentResolver();
contentResolver.unregisterContentObserver(tracksTableObserver);
contentResolver.unregisterContentObserver(waypointsTableObserver);
contentResolver.unregisterContentObserver(trackPointsTableObserver);
if (handlerThread != null) {
handlerThread.getLooper().quit();
handlerThread = null;
}
handler = null;
dataSource = null;
dataSourceManager = null;
}
public void loadTrack(final long trackId) {
@@ -159,15 +195,13 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
* Registers a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
* @param trackDataTypes the track data types
*/
public void registerTrackDataListener(final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean waypointsTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.registerListener(trackDataListener, trackDataTypes);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, waypointsTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
if (started) {
loadDataForListener(trackDataListener);
}
}
@@ -183,22 +217,7 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.unregisterListener(trackDataListener);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
}
}
});
}
/**
* Reloads data for a {@link TrackDataListener}.
*/
public void reloadDataForListener(final TrackDataListener trackDataListener) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
loadDataForListener(trackDataListener);
trackDataManager.unregisterTrackDataListener(trackDataListener);
}
});
}
@@ -217,37 +236,6 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
return selectedTrackId == recordingTrackId && recordingTrackPaused;
}
@Override
public void notifyTracksTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
}
});
}
@Override
public void notifyWaypointsTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
}
});
}
@Override
public void notifyTrackPointsTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
});
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
runInHandlerThread(new Runnable() {
@@ -268,19 +256,17 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
*/
private void loadDataForAll() {
resetSamplingState();
if (trackDataManager.getNumberOfListeners() == 0) {
if (!trackDataManager.hasListeners()) {
return;
}
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
for (TrackDataListener listener : trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
listener.clearTrackPoints();
}
notifyTrackPointsTableUpdate(true,
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
notifyWaypointsTableUpdate(trackDataManager.getListenerWaypoints());
}
/**
@@ -290,14 +276,13 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
*/
private void loadDataForListener(TrackDataListener trackDataListener) {
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
if (trackDataManager.listensForTracks(trackDataListener)) {
notifyTracksTableUpdate(trackDataListeners);
}
boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
if (hasSampledIn || hasSampledOut) {
trackDataListener.clearTrackPoints();
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
@@ -308,7 +293,7 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
}
if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) {
if (trackDataManager.listensForWaypoints(trackDataListener)) {
notifyWaypointsTableUpdate(trackDataListeners);
}
}
@@ -383,60 +368,60 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
}
}
int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L;
long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenLocationId;
int localNumLoadedTrackPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenTrackPointId = updateSamplingState ? firstSeenTrackPointId : -1L;
long localLastSeenTrackPointIdId = updateSamplingState ? lastSeenTrackPointId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenTrackPointId;
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenTrackPointIdId + 1, false)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
long locationId = locationIterator.getTrackPointId();
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
long trackPointId = trackPointIterator.getTrackPointId();
// Stop if past the last wanted point
if (maxPointId != -1L && locationId > maxPointId) {
if (maxPointId != -1L && trackPointId > maxPointId) {
break;
}
if (localFirstSeenLocationId == -1) {
localFirstSeenLocationId = locationId;
if (localFirstSeenTrackPointId == -1) {
localFirstSeenTrackPointId = trackPointId;
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenLocationId);
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenTrackPointId);
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) { //This can be split markers (not anymore supported feature)
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { //This can be split markers (not anymore supported feature)
includeNextPoint = true;
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
if (includeNextPoint || (localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location);
trackDataListener.onSampledInTrackPoint(trackPoint);
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(location);
trackDataListener.onSampledOutTrackPoint(trackPoint);
}
}
}
localNumLoadedPoints++;
localLastSeenLocationId = locationId;
localNumLoadedTrackPoints++;
localLastSeenTrackPointIdId = trackPointId;
}
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedPoints;
firstSeenLocationId = localFirstSeenLocationId;
lastSeenLocationId = localLastSeenLocationId;
numLoadedPoints = localNumLoadedTrackPoints;
firstSeenTrackPointId = localFirstSeenTrackPointId;
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
for (TrackDataListener listener : sampledInListeners) {
@@ -449,16 +434,8 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
*/
private void resetSamplingState() {
numLoadedPoints = 0;
firstSeenLocationId = -1L;
lastSeenLocationId = -1L;
}
/**
* Creates a {@link DataSource}.
*/
@VisibleForTesting
private DataSource newDataSource() {
return new DataSource(context);
firstSeenTrackPointId = -1L;
lastSeenTrackPointId = -1L;
}
/**
@@ -466,6 +443,7 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
*
* @param runnable the runnable
*/
@Deprecated //TODO: Why actually catch this problem: I guess it would be better to fail hard.
@VisibleForTesting
private void runInHandlerThread(Runnable runnable) {
if (handler == null) {
@@ -475,13 +453,4 @@ public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSha
}
handler.post(runnable);
}
/**
* Gets the value selectedTrackId.
*
* @return the selectedTrackId
*/
public long getSelectedTrackId() {
return selectedTrackId;
}
}
@@ -16,9 +16,8 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
@@ -43,20 +42,20 @@ public interface TrackDataListener {
/**
* Called when a sampled in track point is read.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(Location location);
void onSampledInTrackPoint(TrackPoint trackPoint);
/**
* Called when a sampled out track point is read.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(Location location);
void onSampledOutTrackPoint(TrackPoint trackPoint);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)} and {@link #onSampledOutTrackPoint(Location)}.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
*/
void onNewTrackPointsDone();
@@ -16,13 +16,7 @@
package de.dennisguse.opentracks.content;
import android.util.Log;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
/**
@@ -34,84 +28,68 @@ class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName();
private final Map<TrackDataListener, EnumSet<TrackDataType>> listenerToTypesMap = new HashMap<>();
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
private final Set<TrackDataListener> dataListenerWaypoints = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
private final Map<TrackDataType, Set<TrackDataListener>> typeToListenersMap = new EnumMap<>(TrackDataType.class);
TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean waypointsTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
if (tracksTable) dataListenerTracks.add(trackDataListener);
if (waypointsTable) dataListenerWaypoints.add(trackDataListener);
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
void registerListener(TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
dataListenerTracks.add(trackDataListener);
dataListenerWaypoints.add(trackDataListener);
dataListenerTrackPoints_SampledIn.add(trackDataListener);
dataListenerTrackPoints_SampledOut.add(trackDataListener);
}
/**
* Unregisters a listener.
*
* @param listener the listener
*/
void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
return;
}
// Remove the listener from the typeToListenersMap
for (TrackDataType trackDataType : removedTypes) {
typeToListenersMap.get(trackDataType).remove(listener);
}
boolean hasListeners() {
return dataListenerTracks.size() + dataListenerWaypoints.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
}
/**
* Gets the number of {@link TrackDataListener}.
*/
int getNumberOfListeners() {
return listenerToTypesMap.size();
Set<TrackDataListener> listener = new HashSet<>();
listener.addAll(dataListenerTracks);
listener.addAll(dataListenerWaypoints);
listener.addAll(dataListenerTrackPoints_SampledIn);
listener.addAll(dataListenerTrackPoints_SampledOut);
return listener.size();
}
/**
* Gets the track data types for a listener.
*
* @param listener the listener
*/
EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
boolean listensForTracks(TrackDataListener listener) {
return dataListenerTracks.contains(listener);
}
/**
* Gets the listeners for a {@link TrackDataType}.
*
* @param type the type
*/
Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
boolean listensForWaypoints(TrackDataListener listener) {
return dataListenerWaypoints.contains(listener);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
return types;
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
return dataListenerTrackPoints_SampledIn.contains(listener);
}
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
return dataListenerTrackPoints_SampledOut.contains(listener);
}
Set<TrackDataListener> getListenerTracks() {
return dataListenerTracks;
}
Set<TrackDataListener> getListenerWaypoints() {
return dataListenerWaypoints;
}
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
return dataListenerTrackPoints_SampledIn;
}
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
return dataListenerTrackPoints_SampledOut;
}
}
@@ -1,29 +0,0 @@
/*
* Copyright 2011 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.content;
/**
* Types of track data.
*
* @author Jimmy Shih
*/
public enum TrackDataType {
TRACKS_TABLE, // tracks table changes
WAYPOINTS_TABLE, // waypoints table changes
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
}
@@ -16,14 +16,12 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* A track.
@@ -40,10 +38,10 @@ public class Track {
private String icon = "";
private TripStatistics tripStatistics = new TripStatistics();
private TrackStatistics trackStatistics = new TrackStatistics();
// Location points (which may not have been loaded)
private List<Location> locations = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
public Track() {
}
@@ -88,24 +86,24 @@ public class Track {
this.icon = icon;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
public TrackStatistics getTrackStatistics() {
return trackStatistics;
}
public void setTripStatistics(TripStatistics tripStatistics) {
this.tripStatistics = tripStatistics;
public void setTrackStatistics(TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics;
}
@VisibleForTesting
public void addLocation(Location location) {
locations.add(location);
public void addTrackPoint(TrackPoint location) {
trackPoints.add(location);
}
public List<Location> getLocations() {
return locations;
public List<TrackPoint> getTrackPoints() {
return trackPoints;
}
public void setLocations(ArrayList<Location> locations) {
this.locations = locations;
public void setTrackPoints(ArrayList<TrackPoint> trackPoints) {
this.trackPoints = trackPoints;
}
}
@@ -16,6 +16,9 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.location.LocationManager;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -24,17 +27,67 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
*
* @author Sandor Dornbush
*/
public class TrackPoint extends Location {
public class TrackPoint {
private SensorDataSet sensorDataSet = null;
private final Location location;
public TrackPoint(Location location, SensorDataSet sensorDataSet) {
super(location);
public TrackPoint() {
this.location = new Location("");
}
public TrackPoint(@NonNull Location location) {
this.location = location;
}
public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) {
this.location = location;
this.sensorDataSet = sensorDataSet;
}
public TrackPoint(String provider) {
super(provider);
public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) {
this.location = trackPoint.getLocation();
this.sensorDataSet = sensorDataSet;
}
private SensorDataSet sensorDataSet = null;
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitude);
location.setLongitude(longitude);
if (altitude != null) {
location.setAltitude(altitude);
}
location.setTime(time);
}
public static TrackPoint createPause() {
return createPauseWithTime(System.currentTimeMillis());
}
public static TrackPoint createPauseWithTime(long time) {
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(time);
return new TrackPoint(pause);
}
public static TrackPoint createResume() {
return createResumeWithTime(System.currentTimeMillis());
}
public static TrackPoint createResumeWithTime(long time) {
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
resume.setTime(time);
return new TrackPoint(resume);
}
public @NonNull
Location getLocation() {
return location;
}
public SensorDataSet getSensorDataSet() {
@@ -45,8 +98,87 @@ public class TrackPoint extends Location {
this.sensorDataSet = sensorDataSet;
}
public void reset() {
super.reset();
sensorDataSet = null;
public double getLatitude() {
return location.getLatitude();
}
public void setLatitude(double latitude) {
location.setLatitude(latitude);
}
public double getLongitude() {
return location.getLongitude();
}
public void setLongitude(double longitude) {
location.setLongitude(longitude);
}
public long getTime() {
return location.getTime();
}
public void setTime(long time) {
location.setTime(time);
}
public boolean hasAltitude() {
return location.hasAltitude();
}
public double getAltitude() {
return location.getAltitude();
}
public void setAltitude(double altitude) {
location.setAltitude(altitude);
}
public boolean hasSpeed() {
return location.hasSpeed();
}
public float getSpeed() {
return location.getSpeed();
}
public void setSpeed(float speed) {
location.setSpeed(speed);
}
public boolean hasBearing() {
return location.hasBearing();
}
public float getBearing() {
return location.getBearing();
}
public void setBearing(float bearing) {
location.setBearing(bearing);
}
public boolean hasAccuracy() {
return location.hasAccuracy();
}
public float getAccuracy() {
return location.getAccuracy();
}
public void setAccuracy(float horizontalAccuracy) {
location.setAccuracy(horizontalAccuracy);
}
public float distanceTo(@NonNull TrackPoint dest) {
return location.distanceTo(dest.getLocation());
}
public float bearingTo(@NonNull TrackPoint dest) {
return location.bearingTo(dest.getLocation());
}
public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest);
}
}
@@ -19,6 +19,9 @@ package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
/**
* A waypoint.
*
@@ -38,10 +41,15 @@ public final class Waypoint {
private Location location = null;
private String photoUrl = "";
@VisibleForTesting
public Waypoint() {
}
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, Location location, String photoUrl) {
public Waypoint(Location location) {
this.location = location;
}
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, @NonNull Location location, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
@@ -117,14 +125,11 @@ public final class Waypoint {
this.duration = duration;
}
public Location getLocation() {
public @NonNull
Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
public String getPhotoUrl() {
return photoUrl;
}
@@ -41,7 +41,7 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -69,7 +69,7 @@ public class ContentProviderUtils {
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
public static final String ID_SEPARATOR = ",";
private static final String ID_SEPARATOR = ",";
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
@@ -121,7 +121,7 @@ public class ContentProviderUtils {
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
Track track = new Track();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
if (!cursor.isNull(idIndex)) {
track.setId(cursor.getLong(idIndex));
}
@@ -135,31 +135,31 @@ public class ContentProviderUtils {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
trackStatistics.setStartTime_ms(cursor.getLong(startTimeIndex));
}
if (!cursor.isNull(stopTimeIndex)) {
tripStatistics.setStopTime(cursor.getLong(stopTimeIndex));
trackStatistics.setStopTime_ms(cursor.getLong(stopTimeIndex));
}
if (!cursor.isNull(totalDistanceIndex)) {
tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
}
if (!cursor.isNull(totalTimeIndex)) {
tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
trackStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
}
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
trackStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
}
if (!cursor.isNull(minElevationIndex)) {
tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
trackStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
}
if (!cursor.isNull(maxElevationIndex)) {
tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
trackStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
}
if (!cursor.isNull(elevationGainIndex)) {
tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
trackStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
}
if (!cursor.isNull(iconIndex)) {
track.setIcon(cursor.getString(iconIndex));
@@ -308,7 +308,7 @@ public class ContentProviderUtils {
private ContentValues createContentValues(Track track) {
ContentValues values = new ContentValues();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
// Value < 0 indicates no id is available
if (track.getId() >= 0) {
@@ -317,17 +317,17 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.STARTTIME, tripStatistics.getStartTime());
values.put(TracksColumns.STOPTIME, tripStatistics.getStopTime());
values.put(TracksColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, tripStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, tripStatistics.getMovingTime());
values.put(TracksColumns.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, tripStatistics.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, tripStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, tripStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain());
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime_ms());
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime_ms());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, trackStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, trackStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, trackStatistics.getTotalElevationGain());
values.put(TracksColumns.ICON, track.getIcon());
return values;
@@ -356,7 +356,25 @@ public class ContentProviderUtils {
int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
int photoUrlIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.PHOTOURL);
Waypoint waypoint = new Waypoint();
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(timeIndex)) {
location.setTime(cursor.getLong(timeIndex));
}
if (!cursor.isNull(altitudeIndex)) {
location.setAltitude(cursor.getFloat(altitudeIndex));
}
if (!cursor.isNull(accuracyIndex)) {
location.setAccuracy(cursor.getFloat(accuracyIndex));
}
if (!cursor.isNull(bearingIndex)) {
location.setBearing(cursor.getFloat(bearingIndex));
}
Waypoint waypoint = new Waypoint(location);
if (!cursor.isNull(idIndex)) {
waypoint.setId(cursor.getLong(idIndex));
@@ -383,25 +401,6 @@ public class ContentProviderUtils {
waypoint.setDuration(cursor.getLong(durationIndex));
}
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(timeIndex)) {
location.setTime(cursor.getLong(timeIndex));
}
if (!cursor.isNull(altitudeIndex)) {
location.setAltitude(cursor.getFloat(altitudeIndex));
}
if (!cursor.isNull(accuracyIndex)) {
location.setAccuracy(cursor.getFloat(accuracyIndex));
}
if (!cursor.isNull(bearingIndex)) {
location.setBearing(cursor.getFloat(bearingIndex));
}
waypoint.setLocation(location);
if (!cursor.isNull(photoUrlIndex)) {
waypoint.setPhotoUrl(cursor.getString(photoUrlIndex));
}
@@ -622,34 +621,63 @@ public class ContentProviderUtils {
}
/**
* Inserts multiple track points.
* Fills a {@link TrackPoint} from a cursor.
*
* @param locations an array of locations
* @param length the number of locations (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the track id
* @return the number of points inserted
* @param cursor the cursor pointing to a trackPoint.
* @param indexes the cached track points indexes
*/
public int bulkInsertTrackPoint(Location[] locations, int length, long trackId) {
if (length == -1) {
length = locations.length;
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint();
if (!cursor.isNull(indexes.longitudeIndex)) {
trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
}
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(locations[i], trackId);
if (!cursor.isNull(indexes.latitudeIndex)) {
trackPoint.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
if (!cursor.isNull(indexes.timeIndex)) {
trackPoint.setTime(cursor.getLong(indexes.timeIndex));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(cursor.getFloat(indexes.speedIndex));
}
if (!cursor.isNull(indexes.bearingIndex)) {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
}
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
return trackPoint;
}
/**
* Creates a location object from a cursor.
* Inserts multiple trackPoints points.
*
* @param cursor the cursor pointing to the location
* @param trackPoints an array of trackPoints
* @param length the number of trackPoints (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the trackPoints id
* @return the number of points inserted
*/
public Location createTrackPoint(Cursor cursor) {
Location location = new TrackPoint("");
fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor), location);
return location;
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, int length, long trackId) {
if (length == -1) {
length = trackPoints.length;
}
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(trackPoints[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
@@ -722,21 +750,12 @@ public class ContentProviderUtils {
}
/**
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
* Creates a {@link TrackPoint} object from a cursor.
*
* @param trackId the track id
* @param cursor the cursor pointing to the location
*/
@Deprecated
public Location getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
public TrackPoint createTrackPoint(Cursor cursor) {
return fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor));
}
/**
@@ -774,99 +793,71 @@ public class ContentProviderUtils {
return getTrackPointCursor(null, selection, selectionArgs, sortOrder);
}
/**
* Fills a track point from a cursor.
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
*
* @param cursor the cursor pointing to a location.
* @param indexes the cached track points indexes
* @param location the track point
* @param trackId the track id
*/
static void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
location.reset();
if (!cursor.isNull(indexes.longitudeIndex)) {
location.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.latitudeIndex)) {
location.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.timeIndex)) {
location.setTime(cursor.getLong(indexes.timeIndex));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
location.setAltitude(cursor.getFloat(indexes.altitudeIndex));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
location.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
location.setSpeed(cursor.getFloat(indexes.speedIndex));
}
if (!cursor.isNull(indexes.bearingIndex)) {
location.setBearing(cursor.getFloat(indexes.bearingIndex));
}
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
sensorDataSetLocation.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
@Deprecated
public TrackPoint getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackPoint the trackPoint
* @param trackId the track id
* @return the content provider URI of the inserted track point
*/
public Uri insertTrackPoint(Location location, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(location, trackId));
public Uri insertTrackPoint(TrackPoint trackPoint, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
}
/**
* Creates the {@link ContentValues} for a {@link Location}.
* Creates the {@link ContentValues} for a {@link TrackPoint}.
*
* @param location the location
* @param trackPoint the trackPoint
* @param trackId the track id
*/
private ContentValues createContentValues(Location location, long trackId) {
private ContentValues createContentValues(TrackPoint trackPoint, long trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId);
values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
values.put(TrackPointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
values.put(TrackPointsColumns.TIME, trackPoint.getTime());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
}
if (location.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, location.getAccuracy());
if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
}
if (location.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, location.getSpeed());
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed());
}
if (location.hasBearing()) {
values.put(TrackPointsColumns.BEARING, location.getBearing());
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
}
//SensorData
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSetLocation.getSensorDataSet().getHeartRate());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSetLocation.getSensorDataSet().getCadence());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSetLocation.getSensorDataSet().getPower());
}
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower());
}
return values;
}
@@ -875,19 +866,18 @@ public class ContentProviderUtils {
* Creates a new read-only iterator over a given track's points.
* It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* Each call to {@link TrackPointIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link TrackPointFactory#createLocation()} and populates it with information retrieved from the record.
* Each call to {@link TrackPointIterator#next()} may advance to the next DB record.
* When done with iteration, {@link TrackPointIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest location first)
* @param trackPointFactory the location factory
*/
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending, final TrackPointFactory trackPointFactory) {
return new TrackPointIterator(this, trackId, startTrackPointId, descending, trackPointFactory);
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending) {
return new TrackPointIterator(this, trackId, startTrackPointId, descending);
}
private Location findTrackPointBy(String selection, String[] selectionArgs) {
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToNext()) {
return createTrackPoint(cursor);
@@ -15,14 +15,14 @@ import de.dennisguse.opentracks.content.data.WaypointsColumns;
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
@VisibleForTesting
static final int DATABASE_VERSION = 25;
private static final int DATABASE_VERSION = 25;
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
@VisibleForTesting
static final String DATABASE_NAME = "database.db";
private static final String DATABASE_NAME = "database.db";
public CustomSQLiteOpenHelper(Context context) {
@@ -43,7 +43,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static final String TAG = ShareContentProvider.class.getSimpleName();
private static final String TAG = ShareContentProvider.class.getSimpleName();
private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1;
@@ -90,7 +90,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
return new Pair<>(uri, mime);
}
protected static long[] parseURI(Uri uri) {
static long[] parseURI(Uri uri) {
List<String> uriPaths = uri.getPathSegments();
if (uriPaths == null || uriPaths.size() < 2) {
Log.d(TAG, "URI does not contain any trackIds.");
@@ -101,7 +101,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
long[] trackIds = new long[uriTrackIds.length];
for (int i = 0; i < trackIds.length; i++) {
trackIds[i] = Long.valueOf(uriTrackIds[i]);
trackIds[i] = Long.parseLong(uriTrackIds[i]);
}
return trackIds;
}
@@ -151,7 +151,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
}
@Nullable
public static String getTypeMime(@NonNull Uri uri) {
private static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@@ -1,21 +0,0 @@
package de.dennisguse.opentracks.content.provider;
import android.location.LocationManager;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Creates a new {@link TrackPoint}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class TrackPointFactory {
/**
* The default {@link TrackPointFactory} which creates a location each time.
*/
public static final TrackPointFactory DEFAULT_LOCATION_FACTORY = new TrackPointFactory();
public TrackPoint createLocation() {
return new TrackPoint(LocationManager.GPS_PROVIDER);
}
}
@@ -18,21 +18,15 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private final ContentProviderUtils contentProviderUtils;
private final long trackId;
private final boolean descending;
private final TrackPointFactory trackPointFactory;
private final CachedTrackPointsIndexes indexes;
private long lastTrackPointId = -1L;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending, TrackPointFactory trackPointFactory) {
if (trackPointFactory == null) {
throw new IllegalArgumentException("trackPointFactory is null");
}
public TrackPointIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
this.descending = descending;
this.trackPointFactory = trackPointFactory;
cursor = getCursor(startTrackPointId);
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
@@ -91,9 +85,7 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
TrackPoint trackPoint = trackPointFactory.createLocation();
ContentProviderUtils.fillTrackPoint(cursor, indexes, trackPoint);
return trackPoint;
return ContentProviderUtils.fillTrackPoint(cursor, indexes);
}
@Override
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,27 +29,25 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.TrackDataType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* A fragment to display track chart to the user.
* ChartFragment uses a {@link TrackStatisticsUpdater} internally and recomputes the {@link TrackStatistics} from the beginning.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -68,19 +65,19 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return chartFragment;
}
private final List<double[]> pendingPoints = new ArrayList<>();
private final List<ChartPoint> pendingPoints = new ArrayList<>();
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TripStatisticsUpdater tripStatisticsUpdater;
private TrackStatisticsUpdater trackStatisticsUpdater;
private long startTime;
//TODO Why is this needed?
private int recordingDistanceInterval;
// Modes of operation
private boolean chartByDistance;
private final boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements
private ChartView chartView;
@@ -106,8 +103,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
chartView.applyReportSpeed();
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -194,18 +191,18 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null || track.getTripStatistics() == null) {
if (track == null || track.getTrackStatistics() == null) {
startTime = -1L;
return;
}
startTime = track.getTripStatistics().getStartTime();
startTime = track.getTrackStatistics().getStartTime_ms();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
trackStatisticsUpdater = startTime != -1L ? new TrackStatisticsUpdater(startTime) : null;
pendingPoints.clear();
chartView.reset();
runOnUiThread(new Runnable() {
@@ -220,25 +217,25 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
@Override
public void onSampledInTrackPoint(Location location) {
public void onSampledInTrackPoint(TrackPoint trackPoint) {
if (isResumed()) {
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
pendingPoints.add(createPendingPoint(trackPoint));
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
if (isResumed()) {
fillDataPoint(location, null);
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
chartView.addDataPoints(pendingPoints);
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
runOnUiThread(updateChart);
}
@@ -269,46 +266,19 @@ public class ChartFragment extends Fragment implements TrackDataListener {
* Checks the chart settings.
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartView.getReportSpeed())) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, !chartView.getReportSpeed())) {
needUpdate = true;
}
boolean needUpdate = chartView.applyReportSpeed();
if (needUpdate) {
chartView.postInvalidate();
}
}
/**
* Sets the series enabled value.
*
* @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
}
return false;
}
/**
* Resumes the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
trackDataHub.registerTrackDataListener(this, true, true, true, true);
}
/**
@@ -328,16 +298,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Reloads the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* Runs a runnable on the UI thread if possible.
*
@@ -350,82 +310,15 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double[] data) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
if (chartByDistance) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!chartView.getMetricUnits()) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = tripStatistics.getTotalTime();
}
elevation = tripStatisticsUpdater.getSmoothedElevation();
if (!chartView.getMetricUnits()) {
elevation *= UnitConversions.M_TO_FT;
}
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!chartView.getMetricUnits()) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
}
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof TrackPoint && ((TrackPoint) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
}
//TODO: Is related to ChartView.ELEVATION_SERIES etc.
if (data != null) {
data[0] = timeOrDistance;
data[1] = elevation;
data[2] = speed;
data[3] = pace;
data[4] = heartRate;
data[5] = cadence;
data[6] = power;
}
ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits());
}
@VisibleForTesting
void setTripStatisticsUpdater(long time) {
tripStatisticsUpdater = new TripStatisticsUpdater(time);
void setTrackStatisticsUpdater(long time) {
trackStatisticsUpdater = new TrackStatisticsUpdater(time);
}
@VisibleForTesting
@@ -32,7 +32,7 @@ import de.dennisguse.opentracks.util.ResourceUtils;
*
* @author apoorvn
*/
public class ChooseActivityTypeImageAdapter extends BaseAdapter {
class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final List<Integer> imageIds;
private int selected = -1;
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@@ -34,20 +33,18 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.EnumSet;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.TrackDataType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -72,8 +69,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
//TODO Initialize immediately and remove in onDestroy()
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
private TrackPoint lastTrackPoint = null;
private TrackStatistics lastTrackStatistics = null;
private String category = "";
@Deprecated //TODO This should be handled somewhere else; not in the UI.
@@ -96,6 +93,16 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
if (PreferencesUtils.isKey(getContext(), R.string.recording_track_id_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(getContext());
if (PreferencesUtils.getRecordingTrackId(getContext()) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
// A recording track id has been set -> Resumes track and starts timer.
resumeTrackDataHub();
if (trackRecordingServiceConnection == null) {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
}
trackRecordingServiceConnection.startConnection(getContext());
handlerUpdateUI.post(updateUIeachSecond);
}
}
}
};
@@ -114,7 +121,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
updateTotalTime();
updateSensorDataUI();
}
@@ -137,8 +144,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private TextView speedMovingLabel;
private TextView speedMovingValue;
private TextView speedMovingUnit;
private View gradeElevationSeparator;
private View gradeElevationContainer;
private View elevationSeparator;
private View elevationContainer;
private View speedContainer;
private TextView speedLabel;
private TextView speedValue;
@@ -180,8 +187,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator);
gradeElevationContainer = view.findViewById(R.id.stats_elevation_container);
elevationSeparator = view.findViewById(R.id.stats_elevation_separator);
elevationContainer = view.findViewById(R.id.stats_elevation_container);
speedContainer = view.findViewById(R.id.stats_speed);
speedLabel = view.findViewById(R.id.stats_speed_label);
@@ -234,7 +241,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
trackRecordingServiceConnection.startConnection(getContext());
handlerUpdateUI.post(updateUIeachSecond);
@@ -287,8 +294,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
speedMovingValue = null;
speedMovingUnit = null;
gradeElevationSeparator = null;
gradeElevationContainer = null;
elevationSeparator = null;
elevationContainer = null;
speedContainer = null;
speedLabel = null;
@@ -312,7 +319,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUI();
}
@@ -323,17 +330,17 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void clearTrackPoints() {
lastLocation = null;
lastTrackPoint = null;
}
@Override
public void onSampledInTrackPoint(Location location) {
lastLocation = location;
public void onSampledInTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastLocation = location;
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
}
@Override
@@ -344,15 +351,15 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void run() {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastLocation = null;
lastTrackPoint = null;
}
if (lastLocation != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
boolean hasGoodFix = lastLocation.hasAccuracy() && lastLocation.getAccuracy() < recordingGpsAccuracy;
if (lastTrackPoint != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastTrackPoint.getLocation());
boolean hasGoodFix = lastTrackPoint.hasAccuracy() && lastTrackPoint.getAccuracy() < recordingGpsAccuracy;
if (!hasFix || !hasGoodFix) {
lastLocation = null;
lastTrackPoint = null;
}
}
setLocationValues();
@@ -383,8 +390,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
trackDataHub.registerTrackDataListener(this, true, false, true, true);
}
/**
@@ -467,7 +473,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set total distance
{
double totalDistance = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getTotalDistance();
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
distanceValue.setText(parts.first);
@@ -486,14 +492,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
// Set time
if (lastTripStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTripStatistics.getMovingTime()));
if (lastTrackStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTrackStatistics.getMovingTime()));
updateTotalTime();
}
// Set average speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageSpeed() : Double.NaN;
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
@@ -503,7 +509,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set max speed/pace
{
double speed = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxSpeed();
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -514,7 +520,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set moving speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageMovingSpeed() : Double.NaN;
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -525,21 +531,34 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Make elevation visible?
{
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext());
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
elevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
}
}
private void updateTotalTime() {
long totalTime;
if (isSelectedTrackRecording()) {
totalTime = System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime();
totalTime = calculateTotalTime();
} else {
totalTime = lastTripStatistics.getTotalTime();
totalTime = lastTrackStatistics.getTotalTime();
}
totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime));
}
/**
* Return time from service.
* If service isn't bound then use lastTrackStatistics for calculate it.
*/
private long calculateTotalTime() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
return trackRecordingService.getTotalTime();
} else {
return System.currentTimeMillis() - lastTrackStatistics.getStopTime_ms() + lastTrackStatistics.getTotalTime();
}
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
@@ -550,18 +569,18 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isRecording) {
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = lastLocation != null && lastLocation.hasSpeed() ? lastLocation.getSpeed() : Double.NaN;
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
}
// Set elevation
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext());
gradeElevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
boolean showElevation = isRecording && PreferencesUtils.isShowStatsElevation(getContext());
elevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) {
double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN;
if (showElevation) {
double altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? lastTrackPoint.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
elevationValue.setText(parts.first);
@@ -574,11 +593,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastLocation != null ? lastLocation.getLatitude() : Double.NaN;
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
latitudeValue.setText(latitudeText);
double longitude = lastLocation != null ? lastLocation.getLongitude() : Double.NaN;
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
longitudeValue.setText(longitudeText);
}
@@ -40,7 +40,7 @@ import de.dennisguse.opentracks.util.SystemUtils;
* @author Jimmy Shih
*/
//TODO Make independent from ExportActivity?
public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
@@ -70,18 +70,6 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
contentProviderUtils = new ContentProviderUtils(context);
}
/**
* Sets the current activity associated with this AsyncTask.
*
* @param exportActivity the current activity, can be null
*/
public void setActivity(ExportActivity exportActivity) {
this.exportActivity = exportActivity;
if (completed && exportActivity != null) {
exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
}
}
@Override
protected void onPreExecute() {
if (exportActivity != null) {
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -29,7 +28,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -72,9 +70,9 @@ public class FileTrackExporter implements TrackExporter {
writeWaypoints(track1);
}
trackWriter.writeBeginTracks();
long startTime = tracks[0].getTripStatistics().getStartTime();
long startTime = tracks[0].getTrackStatistics().getStartTime_ms();
for (Track track : tracks) {
long offset = track.getTripStatistics().getStartTime() - startTime;
long offset = track.getTrackStatistics().getStartTime_ms() - startTime;
writeLocations(track, offset);
}
trackWriter.writeEndTracks();
@@ -126,25 +124,25 @@ public class FileTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterTrackPointFactory trackPointFactory = new TrackWriterTrackPointFactory();
int locationNumber = 0;
TrackPoint lastTrackPoint = null;
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, trackPointFactory)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
while (locationIterator.hasNext()) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = locationIterator.next();
TrackPoint trackPoint = trackPointIterator.next();
setLocationTime(location, offset);
setLocationTime(trackPoint, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isLocationValid = LocationUtils.isValidLocation(trackPoint.getLocation());
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, trackPointFactory.lastLocation);
trackWriter.writeBeginTrack(track, lastTrackPoint);
wroteTrack = true;
}
@@ -154,14 +152,14 @@ public class FileTrackExporter implements TrackExporter {
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(trackPointFactory.lastLocation);
// Write the previous trackPoint, which we had previously skipped
trackWriter.writeTrackPoint(lastTrackPoint);
}
// Write the current location
trackWriter.writeLocation(location);
// Write the current trackPoint
trackWriter.writeTrackPoint(trackPoint);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, locationIterator.getCount());
trackExporterListener.onProgressUpdate(locationNumber, trackPointIterator.getCount());
}
} else {
if (wroteSegment) {
@@ -169,7 +167,7 @@ public class FileTrackExporter implements TrackExporter {
wroteSegment = false;
}
}
trackPointFactory.swapLocations();
lastTrackPoint = trackPoint;
isLastLocationValid = isLocationValid;
}
@@ -179,7 +177,7 @@ public class FileTrackExporter implements TrackExporter {
}
if (wroteTrack) {
Location lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
@@ -191,41 +189,14 @@ public class FileTrackExporter implements TrackExporter {
}
/**
* Sets a location time.
* Sets a trackPoint time.
*
* @param location the location
* @param offset the time offset
* @param trackPoint the trackPoint
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private class TrackWriterTrackPointFactory extends TrackPointFactory {
TrackPoint currentLocation;
TrackPoint lastLocation;
@Override
public TrackPoint createLocation() {
if (currentLocation == null) {
currentLocation = new TrackPoint("");
}
return currentLocation;
}
void swapLocations() {
TrackPoint tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
private void setLocationTime(TrackPoint trackPoint, long offset) {
if (trackPoint != null) {
trackPoint.setTime(trackPoint.getTime() - offset);
}
}
}
@@ -24,6 +24,7 @@ import java.text.NumberFormat;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.StringUtils;
@@ -139,7 +140,7 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
@@ -150,7 +151,7 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
if (printWriter != null) {
printWriter.println("</trk>");
}
@@ -167,14 +168,14 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeLocation(Location location) {
public void writeTrackPoint(TrackPoint trackPoint) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println("<trkpt " + formatLocation(trackPoint.getLocation()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
"<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@@ -69,12 +69,12 @@ public class KmlTrackWriter implements TrackWriter {
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private Location startLocation;
private TrackPoint startTrackPoint;
/**
* @param context the context
* @param hasMultipleTracks should encode multiple tracks into one file?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportSensorData should {@link SensorDataSet} be exported?
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
*/
@@ -188,11 +188,12 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
this.startLocation = startLocation;
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
this.startTrackPoint = startTrackPoint;
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, startLocation);
Location location = startTrackPoint != null ? startTrackPoint.getLocation() : null;
writePlacemark(name, "", "", START_STYLE, location);
printWriter.println("<Placemark>");
if (exportTrackDetail) {
@@ -210,7 +211,7 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
@@ -218,7 +219,8 @@ public class KmlTrackWriter implements TrackWriter {
if (exportTrackDetail) {
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, false);
writePlacemark(name, "", description, END_STYLE, endLocation);
Location location = endTrackPoint != null ? endTrackPoint.getLocation() : null;
writePlacemark(name, "", description, END_STYLE, location);
}
}
}
@@ -256,16 +258,16 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeLocation(Location location) {
public void writeTrackPoint(TrackPoint trackPoint) {
if (printWriter != null) {
if (exportTrackDetail) {
printWriter.println("<when>" + getTime(location) + "</when>");
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
}
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
if (exportSensorData && location instanceof TrackPoint) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (exportSensorData) {
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
@@ -345,7 +347,7 @@ public class KmlTrackWriter implements TrackWriter {
writeCategory(category);
if (exportPhotos) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(Uri.parse(photoUrl)) + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
@@ -371,7 +373,7 @@ public class KmlTrackWriter implements TrackWriter {
if (exportTrackDetail) {
return StringUtils.formatDateTimeIso8601(location.getTime());
} else {
return StringUtils.formatDateTimeIso8601(location.getTime() - startLocation.getTime());
return StringUtils.formatDateTimeIso8601(location.getTime() - startTrackPoint.getTime());
}
}
@@ -386,7 +388,7 @@ public class KmlTrackWriter implements TrackWriter {
if (trackPointId == -1L) {
return location.getBearing();
}
Location viewLocation;
TrackPoint viewLocation;
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true)) {
if (cursor == null || cursor.getCount() == 0) {
return location.getBearing();
@@ -126,7 +126,7 @@ public class KmzTrackExporter implements TrackExporter {
Uri uri = Uri.parse(photoUrl);
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
ZipEntry zipEntry = new ZipEntry(KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment()));
ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(uri));
zipOutputStream.putNextEntry(zipEntry);
if (inputStream == null) throw new FileNotFoundException();
@@ -147,4 +147,13 @@ public class KmzTrackExporter implements TrackExporter {
outputStream.write(buffer, 0, byteCount);
}
}
/**
* Builds and returns the path for the file that will be saved inside KMZ_IMAGES_DIR.
*
* @param uri URI object.
*/
public static String buildKmzImageFilePath(Uri uri) {
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment());
}
}
@@ -25,8 +25,8 @@ public interface TrackExporterListener {
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* @param number the number of {@link de.dennisguse.opentracks.content.data.TrackPoint}s written
* @param max the maximum number of {@link de.dennisguse.opentracks.content.data.TrackPoint}s in a {@link de.dennisguse.opentracks.content.data.Track}, for calculation of
* completion percentage
*/
void onProgressUpdate(int number, int max);
@@ -15,11 +15,10 @@
*/
package de.dennisguse.opentracks.io.file.exporter;
import android.location.Location;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
@@ -35,13 +34,13 @@ import de.dennisguse.opentracks.content.data.Waypoint;
* {@link #writeEndWaypoints()}
* {@link #writeBeginTracks()}
* For each track:
* {@link #writeBeginTrack(Track, Location)}
* {@link #writeBeginTrack(Track, TrackPoint)}
* For each segment:
* {@link #writeOpenSegment()}
* For each location in the segment:
* {@link #writeLocation(Location)}
* For each trackPoint in the segment:
* {@link #writeTrackPoint(TrackPoint)}
* {@link #writeCloseSegment()}
* {@link #writeEndTrack(Track, Location)}
* {@link #writeEndTrack(Track, TrackPoint)}
* {@link #writeEndTracks()}
* {@link #writeFooter()}
* {@link #close()}
@@ -107,18 +106,18 @@ public interface TrackWriter {
/**
* Writes the beginning of a track.
*
* @param track the track
* @param startLocation the start location
* @param track the track
* @param startTrackPoint the start location
*/
void writeBeginTrack(Track track, Location startLocation);
void writeBeginTrack(Track track, TrackPoint startTrackPoint);
/**
* Writes the end of a track.
*
* @param track the track
* @param endLocation the end location
* @param track the track
* @param endTrackPoint the end location
*/
void writeEndTrack(Track track, Location endLocation);
void writeEndTrack(Track track, TrackPoint endTrackPoint);
/**
* Writes open segment.
@@ -131,9 +130,9 @@ public interface TrackWriter {
void writeCloseSegment();
/**
* Writes a location.
* Writes a trackPoint.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void writeLocation(Location location);
void writeTrackPoint(TrackPoint trackPoint);
}
@@ -17,8 +17,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
@@ -39,12 +37,11 @@ import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -158,12 +155,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
int waypointPosition = -1;
Waypoint waypoint = null;
Location location = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
@Deprecated // TODO Should not be necessary anymore?
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
// TODO Should not be necessary anymore?
TrackStatisticsUpdater markerTrackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
while (true) {
if (waypoint == null) {
@@ -175,38 +172,38 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
if (location == null) {
if (!locationIterator.hasNext()) {
if (trackPoint == null) {
if (!trackPointIterator.hasNext()) {
// No more track points. Ignore the rest of the waypoints.
return;
}
location = locationIterator.next();
trackTripStatisticstrackUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
trackPoint = trackPointIterator.next();
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
markerTrackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if (waypoint.getLocation().getTime() > location.getTime()) {
location = null;
} else if (waypoint.getLocation().getTime() < location.getTime()) {
Log.w(TAG, "Ignoring waypoint: current location was after waypoint.");
if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
trackPoint = null;
} else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
Log.w(TAG, "Ignoring waypoint: current trackPoint was after waypoint.");
waypoint = null;
} else {
// The waypoint location time matches the track point time
if (!LocationUtils.isValidLocation(location)) {
// Invalid location, load the next location
location = null;
// The waypoint trackPoint time matches the track point time
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Invalid trackPoint, load the next trackPoint
trackPoint = null;
continue;
}
// Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude() && location.getLongitude() == waypoint.getLocation().getLongitude()) {
// Valid trackPoint
if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude() && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) {
String waypointDescription = waypoint.getDescription();
String icon = context.getString(R.string.marker_waypoint_icon_url);
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, location, waypoint.getPhotoUrl());
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, trackPoint.getLocation(), waypoint.getPhotoUrl());
contentProviderUtils.insertWaypoint(newWaypoint);
}
@@ -257,11 +254,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (icon != null) {
trackData.track.setIcon(icon);
}
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
if (trackData.trackStatisticsUpdater == null) {
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackData.importTime);
trackData.trackStatisticsUpdater.updateTime(trackData.importTime);
}
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
trackData.track.setTrackStatistics(trackData.trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(trackData.track);
}
@@ -273,7 +270,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
//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) {
insertLocation(createLocation(TrackPointsColumns.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
insertLocation(TrackPoint.createPauseWithTime(trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
@@ -287,13 +284,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return;
}
Waypoint waypoint = new Waypoint();
Location location = createLocation();
TrackPoint trackPoint = createTrackPoint();
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(trackPoint.getLocation());
if (name != null) {
waypoint.setName(name);
@@ -314,12 +310,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
/**
* Gets a track point.
*/
protected Location getTrackPoint() throws SAXException {
Location location = createLocation();
protected TrackPoint getTrackPoint() throws SAXException {
TrackPoint trackPoint = createTrackPoint();
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime();
// Check for negative time change
if (timeDifference <= 0) {
@@ -331,31 +327,31 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* 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.
*/
double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(location) / duration;
location.setSpeed((float) speed);
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed);
}
location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
}
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
// If not the first segment, add a resume separator before adding the first location.
insertLocation(createLocation(TrackPointsColumns.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
insertLocation(TrackPoint.createResumeWithTime(trackPoint.getTime()));
}
trackData.lastLocationInCurrentSegment = location;
return location;
trackData.lastLocationInCurrentSegment = trackPoint;
return trackPoint;
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackPoint the trackPoint
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
protected void insertTrackPoint(TrackPoint trackPoint) {
insertLocation(trackPoint);
}
/**
@@ -370,21 +366,42 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
/**
* Gets the photo url for a file.
*
* @param fileName the file name
* @param externalPhotoUrl the file name
*/
protected String getPhotoUrl(String fileName) {
protected String getInternalPhotoUrl(String externalPhotoUrl) {
if (importTrackId == -1L) {
Log.e(TAG, "Track id is invalid.");
return null;
}
if (externalPhotoUrl == null) {
Log.i(TAG, "External photo url is null.");
return null;
}
Uri externalPhotoUri = Uri.parse(externalPhotoUrl);
if (externalPhotoUri == null) {
Log.w(TAG, "Could not parse external photo url.");
return null;
}
String filename = externalPhotoUri.getLastPathSegment();
if (filename == null) {
Log.w(TAG, "External photo contains no filename.");
return null;
}
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
return Uri.fromFile(file).toString();
File file = new File(dir, filename);
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
/**
* Creates a location.
*/
private Location createLocation() throws SAXException {
private TrackPoint createTrackPoint() throws SAXException {
if (latitude == null || longitude == null) {
return null;
}
@@ -415,45 +432,21 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
}
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
return new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue);
}
/**
* Creates a location.
* Inserts a trackPoint.
*
* @param latitudeValue the latitude value
* @param longitudeValue the longitude value
* @param altitudeValue the altitude value
* @param timeValue the time value
* @param trackPoint the trackPoint
*/
private Location createLocation(double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitudeValue);
location.setLongitude(longitudeValue);
if (altitudeValue != null) {
location.setAltitude(altitudeValue);
} else {
location.removeAltitude();
private void insertLocation(TrackPoint trackPoint) {
if (trackData.trackStatisticsUpdater == null) {
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoint.getTime() != -1L ? trackPoint.getTime() : trackData.importTime);
}
location.setTime(timeValue);
location.removeAccuracy();
location.removeBearing();
location.removeSpeed();
return location;
}
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
/**
* Inserts a location.
*
* @param location the location
*/
private void insertLocation(Location location) {
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(location.getTime() != -1L ? location.getTime() : trackData.importTime);
}
trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
trackData.bufferedLocations[trackData.numBufferedLocations] = location;
trackData.bufferedTrackPoints[trackData.numBufferedLocations] = trackPoint;
trackData.numBufferedLocations++;
trackData.numberOfLocations++;
@@ -471,7 +464,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (data.numBufferedLocations <= 0) {
return;
}
contentProviderUtils.bulkInsertTrackPoint(data.bufferedLocations, data.numBufferedLocations, data.track.getId());
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
}
@@ -490,7 +483,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @author Jimmy Shih
*/
// TODO Why private inner class?
private class TrackData {
private static class TrackData {
// The current track
final Track track = new Track();
@@ -498,19 +491,19 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
int numberOfSegments = 0;
// The last location in the current segment; Null if the current segment doesn't have a last location
Location lastLocationInCurrentSegment;
TrackPoint lastLocationInCurrentSegment;
// The number of locations processed for the current track
int numberOfLocations = 0;
// The trip statistics updater for the current track
TripStatisticsUpdater tripStatisticsUpdater;
// The TrackStatisticsUpdater for the current track
TrackStatisticsUpdater trackStatisticsUpdater;
// The import time of the track.
final long importTime = System.currentTimeMillis();
// The buffered locations
final Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
final TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;
@@ -17,13 +17,13 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
@@ -155,11 +155,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* On track point end.
*/
private void onTrackPointEnd() throws SAXException {
Location location = getTrackPoint();
if (location == null) {
TrackPoint trackPoint = getTrackPoint();
if (trackPoint == null) {
return;
}
insertTrackPoint(location);
insertTrackPoint(trackPoint);
}
/**
@@ -41,7 +41,7 @@ import de.dennisguse.opentracks.util.SystemUtils;
*
* @author Jimmy Shih
*/
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
@@ -81,18 +81,6 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
trackId = -1L;
}
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
*
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
}
}
@Override
protected void onPreExecute() {
if (importActivity != null) {
@@ -17,8 +17,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -65,7 +63,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private boolean trackStarted = false;
private String sensorType;
private ArrayList<Location> locationList;
private ArrayList<TrackPoint> trackPoints;
private ArrayList<Float> cadenceList;
private ArrayList<Float> heartRateList;
private ArrayList<Float> powerList;
@@ -187,10 +185,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
return;
}
if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment());
}
// If there is photoUrl it has to be changed because that url in kml file is a relative path to the internal kmz file.
photoUrl = getInternalPhotoUrl(photoUrl);
addWaypoint();
}
@@ -213,7 +209,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
@Override
protected void onTrackSegmentStart() {
super.onTrackSegmentStart();
locationList = new ArrayList<>();
trackPoints = new ArrayList<>();
heartRateList = new ArrayList<>();
cadenceList = new ArrayList<>();
powerList = new ArrayList<>();
@@ -224,8 +220,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
*/
private void onTrackSegmentEnd() {
// Close a track segment by inserting the segment locations
for (int i = 0; i < locationList.size(); i++) {
Location location = locationList.get(i);
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
boolean hasSensorData = false;
float heartrate = SensorDataSet.DATA_UNAVAILABLE;
@@ -246,9 +242,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
if (!hasSensorData) {
insertTrackPoint(location);
insertTrackPoint(trackPoint);
} else {
TrackPoint sensorDataSetLocation = new TrackPoint(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
TrackPoint sensorDataSetLocation = new TrackPoint(trackPoint, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
}
@@ -270,11 +266,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
Location location = getTrackPoint();
TrackPoint location = getTrackPoint();
if (location == null) {
return;
}
locationList.add(location);
trackPoints.add(location);
time = null;
}
@@ -298,7 +294,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
}
float value;
try {
value = Float.valueOf(content);
value = Float.parseFloat(content);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
@@ -22,7 +22,7 @@ import java.io.InputStream;
*
* @author Jimmy Shih
*/
public interface TrackImporter {
interface TrackImporter {
/**
* Import a file.
@@ -21,7 +21,7 @@ package de.dennisguse.opentracks.services;
*
* @author Sandor Dornbush
*/
public interface LocationListenerPolicy {
interface LocationListenerPolicy {
/**
* Returns the polling interval this policy would like at this moment.
@@ -43,18 +43,16 @@ import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -139,8 +137,8 @@ public class TrackRecordingService extends Service {
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private TripStatisticsUpdater trackTripStatisticsUpdater;
private Location lastLocation;
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
private boolean currentSegmentHasLocation;
private boolean isIdle;
@@ -261,21 +259,21 @@ public class TrackRecordingService extends Service {
return recordingTrackId;
}
public TripStatistics getTripStatistics() {
if (trackTripStatisticsUpdater == null) {
public TrackStatistics getTrackStatistics() {
if (trackStatisticsUpdater == null) {
return null;
}
return trackTripStatisticsUpdater.getTripStatistics();
return trackStatisticsUpdater.getTrackStatistics();
}
public long getTotalTime() {
if (trackTripStatisticsUpdater == null) {
if (trackStatisticsUpdater == null) {
return 0;
}
if (!isPaused()) {
trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
trackStatisticsUpdater.updateTime(System.currentTimeMillis());
}
return trackTripStatisticsUpdater.getTripStatistics().getTotalTime();
return trackStatisticsUpdater.getTrackStatistics().getTotalTime();
}
/**
@@ -296,9 +294,9 @@ public class TrackRecordingService extends Service {
name = getString(R.string.marker_name_format, nextWaypointNumber + 1);
}
Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId);
if (location == null) {
Log.i(TAG, "Could not create a waypoint as location is unknown.");
TrackPoint trackPoint = getLastValidTrackPointInCurrentSegment(recordingTrackId);
if (trackPoint == null) {
Log.i(TAG, "Could not create a waypoint as trackPoint is unknown.");
return -1L;
}
@@ -307,12 +305,12 @@ public class TrackRecordingService extends Service {
String icon = getString(R.string.marker_waypoint_icon_url);
photoUrl = photoUrl != null ? photoUrl : "";
TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics();
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
double length = stats.getTotalDistance();
long duration = stats.getTotalTime();
// Insert waypoint
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, location, photoUrl);
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, trackPoint.getLocation(), photoUrl);
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return ContentUris.parseId(uri);
}
@@ -328,7 +326,7 @@ public class TrackRecordingService extends Service {
return -1L;
}
long now = System.currentTimeMillis();
trackTripStatisticsUpdater = new TripStatisticsUpdater(now);
trackStatisticsUpdater = new TrackStatisticsUpdater(now);
// Insert a track
Track track = new Track();
@@ -345,21 +343,46 @@ public class TrackRecordingService extends Service {
String category = PreferencesUtils.getDefaultActivity(this);
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track);
startRecording();
return trackId;
}
/**
* Resumes the track identified by trackId.
* It results in a pause/continue.
*
* @param trackId the id of the track to be resumed.
*/
void resumeTrack(long trackId) {
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Ignore resumeTrack. Track " + trackId + " does not exists.");
return;
}
// Sync the real time setting the stop time with current time.
track.getTrackStatistics().setStopTime_ms(System.currentTimeMillis());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, TrackPoint.createPause(), null);
insertTrackPoint(track, TrackPoint.createResume(), null);
// Update shared preferences.
updateRecordingState(trackId, false);
startRecording();
}
private void restartTrack(Track track) {
Log.d(TAG, "Restarting track: " + track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
trackTripStatisticsUpdater.addLocation(locationIterator, recordingDistanceInterval);
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
trackStatisticsUpdater.addTrackPoint(locationIterator, recordingDistanceInterval);
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
}
@@ -379,11 +402,7 @@ public class TrackRecordingService extends Service {
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
resume.setTime(System.currentTimeMillis());
insertLocation(track, resume, null);
insertTrackPoint(track, TrackPoint.createResume(), null);
}
startRecording();
@@ -396,7 +415,7 @@ public class TrackRecordingService extends Service {
// Update instance variables
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
lastLocation = null;
lastTrackPoint = null;
currentSegmentHasLocation = false;
isIdle = false;
@@ -436,13 +455,13 @@ public class TrackRecordingService extends Service {
if (track != null) {
// If not paused, add the last location
if (!paused) {
insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId));
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(trackId));
// Update the recording track time
updateRecordingTrack(track);
}
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime());
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTrackStatistics().getStartTime_ms());
if (trackName != null && !trackName.equals(track.getName())) {
track.setName(trackName);
contentProviderUtils.updateTrack(track);
@@ -464,13 +483,9 @@ public class TrackRecordingService extends Service {
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(track.getId()));
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(track.getId()));
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(System.currentTimeMillis());
insertLocation(track, pause, null);
insertTrackPoint(track, TrackPoint.createPause(), null);
}
endRecording(false);
@@ -492,7 +507,7 @@ public class TrackRecordingService extends Service {
remoteSensorManager.stop();
remoteSensorManager = null;
}
lastLocation = null;
lastTrackPoint = null;
stopGps(trackStopped);
}
@@ -519,7 +534,7 @@ public class TrackRecordingService extends Service {
* @param trackId the track id
* @return the location or null
*/
private Location getLastValidTrackPointInCurrentSegment(long trackId) {
private TrackPoint getLastValidTrackPointInCurrentSegment(long trackId) {
if (!currentSegmentHasLocation) {
return null;
}
@@ -556,7 +571,8 @@ public class TrackRecordingService extends Service {
return;
}
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet());
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
@@ -569,87 +585,78 @@ public class TrackRecordingService extends Service {
location.setTime(System.currentTimeMillis());
}
Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L;
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
idleTime = location.getTime() - lastValidTrackPoint.getTime();
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getLocation().getTime()) {
idleTime = location.getTime() - lastValidTrackPoint.getLocation().getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet != null) {
location = new TrackPoint(location, sensorDataSet);
}
// Always insert the first segment location
if (!currentSegmentHasLocation) {
insertLocation(track, location, null);
insertTrackPoint(track, trackPoint, null);
currentSegmentHasLocation = true;
lastLocation = location;
lastTrackPoint = trackPoint;
return;
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
if (!LocationUtils.isValidLocation(lastValidTrackPoint.getLocation())) {
// Should not happen. The current segment should have a location. Just insert the current location.
insertLocation(track, location, null);
lastLocation = location;
insertTrackPoint(track, trackPoint, null);
lastTrackPoint = trackPoint;
return;
}
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint.getLocation());
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, TrackPoint.createPause(), null);
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(lastLocation.getTime());
insertLocation(track, pause, null);
insertLocation(track, location, null);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
} else if (trackPoint.getSensorDataSet() != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = true;
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else {
Log.d(TAG, "Not recording location, idle");
}
lastLocation = location;
lastTrackPoint = trackPoint;
}
/**
* Inserts a location.
* Inserts a trackPoint.
*
* @param track the track
* @param location the location
* @param trackPoint the trackPoint
* @param lastValidTrackPoint the last valid track point, can be null
*/
private void insertLocation(Track track, Location location, Location lastValidTrackPoint) {
if (location == null) {
Log.w(TAG, "Ignore insertLocation. location is null.");
private void insertTrackPoint(Track track, TrackPoint trackPoint, TrackPoint lastValidTrackPoint) {
if (trackPoint == null) {
Log.w(TAG, "Ignore insertLocation. trackPoint is null.");
return;
}
// Do not insert if inserted already
if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == location.getTime()) {
Log.w(TAG, "Ignore insertLocation. location time same as last valid track point time.");
if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == trackPoint.getTime()) {
Log.w(TAG, "Ignore insertLocation. trackPoint time same as last valid track point time.");
return;
}
try {
contentProviderUtils.insertTrackPoint(location, track.getId());
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
updateRecordingTrack(track);
} catch (SQLiteException e) {
/*
@@ -667,8 +674,8 @@ public class TrackRecordingService extends Service {
* @param track the track
*/
private void updateRecordingTrack(Track track) {
trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
trackStatisticsUpdater.updateTime(System.currentTimeMillis());
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track);
}
@@ -36,6 +36,11 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
return trackRecordingService.startNewTrack();
}
@Override
public void resumeTrack(long trackId) {
trackRecordingService.resumeTrack(trackId);
}
@Override
public void pauseCurrentTrack() {
trackRecordingService.pauseCurrentTrack();
@@ -78,7 +83,7 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
@VisibleForTesting
@Override
public void insertTrackPoint(Location location) {
public void insertLocation(Location location) {
trackRecordingService.onLocationChangedAsync(location);
}
@@ -54,10 +54,9 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Constructor.
*
* @param context the context
* @param callback the callback to invoke when the service binding changes
*/
public TrackRecordingServiceConnection(Context context, Runnable callback) {
public TrackRecordingServiceConnection(Runnable callback) {
this.callback = callback;
}
@@ -71,7 +70,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Binds the service if it is started.
*/
public void bindIfStarted(Context context) {
private void bindIfStarted(Context context) {
bindService(context, false);
}
@@ -44,6 +44,13 @@ public interface TrackRecordingServiceInterface {
*/
long startNewTrack();
/**
* Resumes the track identified by trackId.
*
* @param trackId
*/
void resumeTrack(long trackId);
/**
* Pauses the current recording track.
*/
@@ -89,15 +96,15 @@ public interface TrackRecordingServiceInterface {
/**
* ONLY FOR TESTING
*
* Inserts a track point in the current recording track.
* Inserts a location in the current recording track.
* <p>
* When recording a track, GPS locations are automatically inserted. This is used for
* inserting special track points or for testing.
* When recording a track, GPS locations are automatically inserted.
* This is used for inserting special track points or for testing.
*
* @param location the track point to be inserted
* @param location the location to be inserted
*/
@VisibleForTesting
void insertTrackPoint(Location location);
void insertLocation(Location location);
/**
* Gets the current sensor data. Returns null if there is no data.
@@ -5,13 +5,13 @@ import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -63,12 +63,12 @@ class TrackRecordingServiceNotificationManager {
updateNotification();
}
void updateLocation(Context context, Location location, int recordingGpsAccuracy) {
void updateTrackPoint(Context context, TrackPoint trackPoint, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (location.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, location.getAccuracy(), PreferencesUtils.isMetricUnits(context));
if (trackPoint.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), PreferencesUtils.isMetricUnits(context));
boolean currentLocationWasAccurate = location.getAccuracy() < recordingGpsAccuracy;
boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
@@ -26,11 +26,11 @@ import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils;
/**
* This class will periodically announce the user's trip statistics.
* This class will periodically announce the user's {@link TrackStatistics}.
*
* @author Sandor Dornbush
*/
@@ -126,17 +126,17 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
announce(trackRecordingService.getTripStatistics());
announce(trackRecordingService.getTrackStatistics());
}
/**
* Runs this task.
*
* @param tripStatistics the trip statistics
* @param trackStatistics the track statistics
*/
private void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null.");
private void announce(TrackStatistics trackStatistics) {
if (trackStatistics == null) {
Log.e(TAG, "TrackStatistics is null.");
return;
}
@@ -157,7 +157,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
String announcement = AnnouncementUtils.getAnnouncement(context, tripStatistics);
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics);
speakAnnouncement(announcement);
}
@@ -19,7 +19,7 @@ import android.util.Log;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -30,7 +30,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
public class PeriodicTaskExecutor {
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName();
private static final long MINUTE_TO_MILLISECONDS = 60000L;
private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory;
@@ -93,7 +92,7 @@ public class PeriodicTaskExecutor {
if (timerTaskExecutor == null) {
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
}
timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
timerTaskExecutor.scheduleTask(taskFrequency * UnitConversions.ONE_MINUTE_MS);
} else {
// For distance periodic task
calculateNextTaskDistance();
@@ -122,12 +121,12 @@ public class PeriodicTaskExecutor {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) {
return;
}
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
@@ -166,8 +165,8 @@ public class PeriodicTaskExecutor {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) {
return;
}
@@ -177,7 +176,7 @@ public class PeriodicTaskExecutor {
return;
}
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
@@ -23,7 +23,7 @@ import android.content.Context;
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
interface PeriodicTaskFactory {
/**
* Creates a {@link PeriodicTask}.
@@ -21,7 +21,7 @@ import java.util.Timer;
import java.util.TimerTask;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* This class will periodically perform a task.
@@ -55,8 +55,8 @@ class TimerTaskExecutor {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
TrackStatistics trackStatistics = trackRecordingService.getTrackStatistics();
if (trackStatistics == null) {
return;
}
@@ -69,7 +69,7 @@ class TimerTaskExecutor {
}
};
timer = new Timer(TimerTaskExecutor.class.getSimpleName());
long next = System.currentTimeMillis() + interval_ms - (tripStatistics.getTotalTime() % interval_ms);
long next = System.currentTimeMillis() + interval_ms - (trackStatistics.getTotalTime() % interval_ms);
timer.scheduleAtFixedRate(timerTask, new Date(next), interval_ms);
}
@@ -72,7 +72,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
return devices.get(index);
}
public class Device {
public static class Device {
private String name;
private final String address;
@@ -25,7 +25,7 @@ import androidx.annotation.NonNull;
*
* @author Sandor Dornbush
*/
public class DoubleBuffer {
class DoubleBuffer {
// The sliding buffer of doubles.
private final double[] buffer;
@@ -19,23 +19,20 @@ package de.dennisguse.opentracks.stats;
import androidx.annotation.NonNull;
/**
* Statistical data about a trip.
* The data in this class should be filled out by TripStatisticsBuilder.
* Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}.
* The data in this class should be filled out by {@link TrackStatisticsUpdater}.
*
* @author Rodrigo Damazio
*/
public class TripStatistics {
public class TrackStatistics {
// The min and max elevation (meters) seen on this trip.
// The min and max elevation (meters) seen on this track.
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
// The min and max grade seen on this trip.
@Deprecated //Remove grade min/max completely
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
// The trip start time. This is the system time, might not match the GPs time.
private long startTime = -1L;
// The trip stop time. This is the system time, might not match the GPS time.
private long stopTime = -1L;
// The track start time. This is the system time, might not match the GPs time.
private long startTime_ms = -1L;
// The track stop time. This is the system time, might not match the GPS time.
private long stopTime_ms = -1L;
private double totalDistance_m;
// Updated when new points are received, may be stale.
@@ -47,7 +44,7 @@ public class TripStatistics {
// The total elevation gained (meters).
private double totalElevationGain_m;
public TripStatistics() {
public TrackStatistics() {
}
/**
@@ -55,16 +52,15 @@ public class TripStatistics {
*
* @param other another statistics data object to copy from
*/
public TripStatistics(TripStatistics other) {
startTime = other.startTime;
stopTime = other.stopTime;
public TrackStatistics(TrackStatistics other) {
startTime_ms = other.startTime_ms;
stopTime_ms = other.stopTime_ms;
totalDistance_m = other.totalDistance_m;
totalTime_ms = other.totalTime_ms;
movingTime_ms = other.movingTime_ms;
maxSpeed_mps = other.maxSpeed_mps;
elevationExtremities.set(other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
totalElevationGain_m = other.totalElevationGain_m;
gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax());
}
/**
@@ -73,9 +69,9 @@ public class TripStatistics {
*
* @param other another statistics data object
*/
public void merge(TripStatistics other) {
startTime = Math.min(startTime, other.startTime);
stopTime = Math.max(stopTime, other.stopTime);
public void merge(TrackStatistics other) {
startTime_ms = Math.min(startTime_ms, other.startTime_ms);
stopTime_ms = Math.max(stopTime_ms, other.stopTime_ms);
totalDistance_m += other.totalDistance_m;
totalTime_ms += other.totalTime_ms;
movingTime_ms += other.movingTime_ms;
@@ -85,29 +81,25 @@ public class TripStatistics {
elevationExtremities.update(other.elevationExtremities.getMax());
}
totalElevationGain_m += other.totalElevationGain_m;
if (other.gradeExtremities.hasData()) {
gradeExtremities.update(other.gradeExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
}
}
/**
* Gets the trip start time. The number of milliseconds since epoch.
* Gets the track start time. The number of milliseconds since epoch.
*/
public long getStartTime() {
return startTime;
public long getStartTime_ms() {
return startTime_ms;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
public void setStartTime_ms(long startTime_ms) {
this.startTime_ms = startTime_ms;
}
public long getStopTime() {
return stopTime;
public long getStopTime_ms() {
return stopTime_ms;
}
public void setStopTime(long stopTime) {
this.stopTime = stopTime;
public void setStopTime_ms(long stopTime_ms) {
this.stopTime_ms = stopTime_ms;
}
public double getTotalDistance() {
@@ -125,7 +117,7 @@ public class TripStatistics {
/**
* Gets the total time in milliseconds that this track has been active.
* This statistic is only updated when a new point is added to the statistics, so it may be off.
* If you need to calculate the proper total time, use {@link #getStartTime} with the current time.
* If you need to calculate the proper total time, use {@link #getStartTime_ms} with the current time.
*/
public long getTotalTime() {
return totalTime_ms;
@@ -242,39 +234,13 @@ public class TripStatistics {
totalElevationGain_m += gain_m;
}
public double getMinGrade() {
return gradeExtremities.getMin();
}
public void setMinGrade(double grade) {
gradeExtremities.setMin(grade);
}
public double getMaxGrade() {
return gradeExtremities.getMax();
}
/**
* Sets the maximum grade.
*
* @param grade the grade as a fraction (1.0 would mean vertical upwards)
*/
public void setMaxGrade(double grade) {
gradeExtremities.setMax(grade);
}
public void updateGradeExtremities(double grade) {
gradeExtremities.update(grade);
}
@NonNull
@Override
public String toString() {
return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
return "TrackStatistics { Start Time: " + getStartTime_ms() + "; Stop Time: " + getStopTime_ms()
+ "; Total Distance: " + getTotalDistance() + "; Total Time: " + getTotalTime()
+ "; Moving Time: " + getMovingTime() + "; Max Speed: " + getMaxSpeed()
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
+ "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade()
+ "; Max Grade: " + getMaxGrade() + "}";
+ "; Elevation Gain: " + getTotalElevationGain() + "}";
}
}
@@ -16,11 +16,11 @@
package de.dennisguse.opentracks.stats;
import android.location.Location;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -28,38 +28,28 @@ import de.dennisguse.opentracks.util.LocationUtils;
import static de.dennisguse.opentracks.services.TrackRecordingService.MAX_NO_MOVEMENT_SPEED;
/**
* Updater for {@link TripStatistics}.
* For updating track trip statistics as new locations are added.
* NOTE:Some of the locations represent pause/resume separator.
* Updater for {@link TrackStatistics}.
* For updating track {@link TrackStatistics} as new {@link TrackPoint}s are added.
* NOTE: Some of the locations represent pause/resume separator.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class TripStatisticsUpdater {
public class TrackStatisticsUpdater {
/**
* The number of grade readings to smooth to get a somewhat accurate signal.
*/
public static final int GRADE_SMOOTHING_FACTOR = 5;
/**
* The number of elevation readings to smooth to get a somewhat accurate
* signal.
* The number of elevation readings to smooth to get a somewhat accurate signal.
*/
@VisibleForTesting
private static final int ELEVATION_SMOOTHING_FACTOR = 25;
/**
* The number of run readings to smooth for calculating grade.
*/
@VisibleForTesting
private static final int RUN_SMOOTHING_FACTOR = 25;
/**
* The number of speed reading to smooth to get a somewhat accurate signal.
*/
@VisibleForTesting
private static final int SPEED_SMOOTHING_FACTOR = 25;
private static final String TAG = TripStatisticsUpdater.class.getSimpleName();
private static final String TAG = TrackStatisticsUpdater.class.getSimpleName();
/**
* Ignore any acceleration faster than this.
* Will ignore any speeds that imply acceleration greater than 2g's
@@ -67,96 +57,100 @@ public class TripStatisticsUpdater {
*/
private static final double MAX_ACCELERATION = 0.02;
// The track's trip statistics
private final TripStatistics tripStatistics;
// The track's statistics
private final TrackStatistics trackStatistics;
// A buffer of the recent elevation readings (m)
private final DoubleBuffer elevationBuffer = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR);
// A buffer of the recent run readings (m) for calculating grade
private final DoubleBuffer runBuffer = new DoubleBuffer(RUN_SMOOTHING_FACTOR);
// A buffer of the recent grade calculations (%)
private final DoubleBuffer gradeBuffer = new DoubleBuffer(GRADE_SMOOTHING_FACTOR);
private final DoubleBuffer elevationBuffer_m = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR);
// 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_ms = new DoubleBuffer(SPEED_SMOOTHING_FACTOR);
// The current segment's trip statistics
private TripStatistics currentSegment;
// Current segment's last location.
private Location lastLocation;
// Current segment's last moving location
private Location lastMovingLocation;
// The current segment's statistics
private TrackStatistics currentSegment;
// Current segment's last trackPoint
private TrackPoint lastTrackPoint;
// Current segment's last moving trackPoint
private TrackPoint lastMovingTrackPoint;
/**
* Creates a new trip statistics updater.
* Creates a new {@link TrackStatisticsUpdater}.
*
* @param startTime the start time
* @param startTime_ms the start time in milliseconds
*/
public TripStatisticsUpdater(long startTime) {
tripStatistics = init(startTime);
currentSegment = init(startTime);
public TrackStatisticsUpdater(long startTime_ms) {
trackStatistics = init(startTime_ms);
currentSegment = init(startTime_ms);
}
/**
* Creates a new{@link TrackStatisticsUpdater} with a {@link TrackStatisticsUpdater} already existed.
*
* @param trackStatistics a {@link TrackStatisticsUpdater}
*/
public TrackStatisticsUpdater(TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics;
currentSegment = init(System.currentTimeMillis());
}
public void updateTime(long time) {
currentSegment.setStopTime(time);
currentSegment.setTotalTime(time - currentSegment.getStartTime());
currentSegment.setStopTime_ms(time);
currentSegment.setTotalTime(time - currentSegment.getStartTime_ms());
}
/**
* Gets the track's trip statistics.
* Gets the track's statistics.
*/
public TripStatistics getTripStatistics() {
// Take a snapshot - we don't want anyone messing with our tripStatistics
TripStatistics stats = new TripStatistics(tripStatistics);
public TrackStatistics getTrackStatistics() {
// Take a snapshot - we don't want anyone messing with our trackStatistics
TrackStatistics stats = new TrackStatistics(trackStatistics);
stats.merge(currentSegment);
return stats;
}
/**
* Adds a location.
* TODO: This assume location has a valid time.
* Adds a trackPoint.
* TODO: This assume trackPoint has a valid time.
*
* @param location the location
* @param trackPoint the trackPoint
* @param minRecordingDistance the min recording distance
*/
public void addLocation(Location location, int minRecordingDistance) {
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
// Always update time
updateTime(location.getTime());
if (!LocationUtils.isValidLocation(location)) {
updateTime(trackPoint.getLocation().getTime());
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Either pause or resume marker
if (location.getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (lastLocation != null && lastMovingLocation != null && lastLocation != lastMovingLocation) {
currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation));
if (trackPoint.getLocation().getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) {
currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint));
}
tripStatistics.merge(currentSegment);
trackStatistics.merge(currentSegment);
}
currentSegment = init(location.getTime());
lastLocation = null;
lastMovingLocation = null;
elevationBuffer.reset();
runBuffer.reset();
gradeBuffer.reset();
speedBuffer.reset();
currentSegment = init(trackPoint.getLocation().getTime());
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
return;
}
//TODO Use Barometer to compute elevation gain.
double elevationDifference = location.hasAltitude() ? updateElevation(location.getAltitude()) : 0.0;
double elevationDifference = trackPoint.hasAltitude() ? updateElevation(trackPoint.getAltitude()) : 0.0;
if (lastLocation == null || lastMovingLocation == null) {
lastLocation = location;
lastMovingLocation = location;
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
return;
}
double movingDistance = lastMovingLocation.distanceTo(location);
if (movingDistance < minRecordingDistance && (!location.hasSpeed() || location.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
speedBuffer.reset();
lastLocation = location;
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && (!trackPoint.hasSpeed() || trackPoint.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
}
long movingTime = location.getTime() - lastLocation.getTime();
long movingTime = trackPoint.getTime() - lastTrackPoint.getTime();
if (movingTime < 0) {
lastLocation = location;
lastTrackPoint = trackPoint;
return;
}
@@ -166,23 +160,19 @@ public class TripStatisticsUpdater {
// Update moving time
currentSegment.addMovingTime(movingTime);
// Update grade
double run = lastLocation.distanceTo(location);
updateGrade(run, elevationDifference);
// Update max speed
if (location.hasSpeed() && lastLocation.hasSpeed()) {
updateSpeed(location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
if (trackPoint.hasSpeed() && lastTrackPoint.hasSpeed()) {
updateSpeed(trackPoint.getTime(), trackPoint.getSpeed(), lastTrackPoint.getTime(), lastTrackPoint.getSpeed());
}
lastLocation = location;
lastMovingLocation = location;
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
}
public void addLocation(TrackPointIterator iterator, int minRecordingDistance) {
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
Location location = iterator.next();
addLocation(location, minRecordingDistance);
TrackPoint location = iterator.next();
addTrackPoint(location, minRecordingDistance);
}
}
@@ -191,11 +181,11 @@ public class TripStatisticsUpdater {
* The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks.
*/
public double getSmoothedElevation() {
return elevationBuffer.getAverage();
return elevationBuffer_m.getAverage();
}
public double getSmoothedSpeed() {
return speedBuffer.getAverage();
return speedBuffer_ms.getAverage();
}
/**
@@ -209,11 +199,11 @@ public class TripStatisticsUpdater {
@VisibleForTesting
private void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
if (speed < MAX_NO_MOVEMENT_SPEED) {
speedBuffer.reset();
speedBuffer_ms.reset();
} else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) {
speedBuffer.setNext(speed);
if (speedBuffer.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer.getAverage());
speedBuffer_ms.setNext(speed);
if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer_ms.getAverage());
}
} else {
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed);
@@ -228,9 +218,9 @@ public class TripStatisticsUpdater {
@VisibleForTesting
private double updateElevation(double elevation) {
// Update elevation using the smoothed average
double oldAverage = elevationBuffer.getAverage();
elevationBuffer.setNext(elevation);
double newAverage = elevationBuffer.getAverage();
double oldAverage = elevationBuffer_m.getAverage();
elevationBuffer_m.setNext(elevation);
double newAverage = elevationBuffer_m.getAverage();
currentSegment.updateElevationExtremities(newAverage);
double difference = newAverage - oldAverage;
@@ -240,30 +230,10 @@ public class TripStatisticsUpdater {
return difference;
}
/**
* Updates a grade reading.
*
* @param run the run
* @param rise the rise
*/
@VisibleForTesting
private void updateGrade(double run, double rise) {
runBuffer.setNext(run);
double smoothedRun = runBuffer.getAverage();
// With the error in the altitude measurement, it is dangerous to divide by * anything less than 5.
if (smoothedRun < 5.0) {
return;
}
gradeBuffer.setNext(rise / smoothedRun);
currentSegment.updateGradeExtremities(gradeBuffer.getAverage());
}
private TripStatistics init(long time) {
TripStatistics stats = new TripStatistics();
stats.setStartTime(time);
stats.setStopTime(time);
private TrackStatistics init(long time) {
TrackStatistics stats = new TrackStatistics();
stats.setStartTime_ms(time);
stats.setStopTime_ms(time);
return stats;
}
@@ -294,8 +264,8 @@ public class TripStatisticsUpdater {
}
// 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()) {
double average = speedBuffer.getAverage();
if (speedBuffer_ms.isFull()) {
double average = speedBuffer_ms.getAverage();
double diff = Math.abs(average - speed);
return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
}
@@ -3,17 +3,17 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
public class AnnouncementUtils {
private AnnouncementUtils() {}
public static String getAnnouncement(Context context, TripStatistics tripStatistics) {
public static String getAnnouncement(Context context, TrackStatistics trackStatistics) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (distance == 0) {
return context.getString(R.string.voice_total_distance_zero);
@@ -38,10 +38,10 @@ public class AnnouncementUtils {
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(context, tripStatistics.getMovingTime()), rate);
return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(context, trackStatistics.getMovingTime()), rate);
}
public static String getAnnounceTime(Context context, long time) {
private static String getAnnounceTime(Context context, long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources()
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
@@ -71,7 +71,7 @@ public class AnnouncementUtils {
*
* @param d the double value
*/
public static int getQuantityCount(double d) {
private static int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
@@ -81,7 +81,7 @@ public class AnnouncementUtils {
} else {
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
int count = (int) d;
return count < 3 ? 3 : count;
return Math.max(count, 3);
}
}
}
@@ -27,7 +27,6 @@ import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -231,9 +230,8 @@ public class FileUtils {
*
* @param src source file.
* @param dst destination file.
* @throws IOException
*/
public static void copy(FileDescriptor src, File dst) throws IOException {
*/
public static void copy(FileDescriptor src, File dst) {
try (FileChannel in = new FileInputStream(src).getChannel();
FileChannel out = new FileOutputStream(dst).getChannel()) {
in.transferTo(0, in.size(), out);
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Stack;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Utility class for decimating tracks at a given level of precision.
@@ -84,16 +85,16 @@ public class LocationUtils {
}
/**
* Decimates the given locations for a given zoom level.
* Decimates the given trackPoints for a given zoom level.
* This uses a Douglas-Peucker decimation algorithm.
*
* @param tolerance in meters
* @param locations input
* @param trackPoints input
* @param decimated output
*/
//TODO What was it used for? Sharing data with other apps?
private static void decimate(double tolerance, List<Location> locations, List<Location> decimated) {
final int n = locations.size();
private static void decimate(double tolerance, List<TrackPoint> trackPoints, List<TrackPoint> decimated) {
final int n = trackPoints.size();
if (n < 1) {
return;
}
@@ -114,8 +115,7 @@ public class LocationUtils {
current = stack.pop();
maxDist = 0;
for (idx = current[0] + 1; idx < current[1]; ++idx) {
dist = LocationUtils.distance(
locations.get(idx), locations.get(current[0]), locations.get(current[1]));
dist = LocationUtils.distance(trackPoints.get(idx).getLocation(), trackPoints.get(current[0]).getLocation(), trackPoints.get(current[1]).getLocation());
if (dist > maxDist) {
maxDist = dist;
maxIdx = idx;
@@ -134,7 +134,7 @@ public class LocationUtils {
int i = 0;
idx = 0;
decimated.clear();
for (Location l : locations) {
for (TrackPoint l : trackPoints) {
if (dists[idx] != 0) {
decimated.add(l);
i++;
@@ -151,9 +151,9 @@ public class LocationUtils {
* @param precision desired precision in meters
*/
public static void decimate(Track track, double precision) {
ArrayList<Location> decimated = new ArrayList<>();
decimate(precision, track.getLocations(), decimated);
track.setLocations(decimated);
ArrayList<TrackPoint> decimated = new ArrayList<>();
decimate(precision, track.getTrackPoints(), decimated);
track.setTrackPoints(decimated);
}
/**
@@ -164,7 +164,7 @@ public class LocationUtils {
* @param location the location to test
* @return true if the location is a valid location.
*/
//TODO Still needed?
//TODO Move to TrackPoint
public static boolean isValidLocation(Location location) {
return location != null
&& Math.abs(location.getLatitude()) <= 90
@@ -93,7 +93,7 @@ public class PreferencesUtils {
* @param keyId the key id
* @param defaultValue the default value
*/
public static boolean getBoolean(Context context, int keyId, boolean defaultValue) {
private static boolean getBoolean(Context context, int keyId, boolean defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
}
@@ -119,7 +119,7 @@ public class PreferencesUtils {
* @param keyId the key id
* @param defaultValue the default value
*/
public static int getInt(Context context, int keyId, int defaultValue) {
private static int getInt(Context context, int keyId, int defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
try {
@@ -215,7 +215,7 @@ public class PreferencesUtils {
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
public static String getBluetoothHeartRateSensorAddressNone(Context context) {
private static String getBluetoothHeartRateSensorAddressNone(Context context) {
return context.getString(R.string.sensor_type_value_none);
}
@@ -241,9 +241,9 @@ public class PreferencesUtils {
return getBoolean(context, R.string.stats_keep_screen_on_while_recording_key, DEFAULT);
}
public static boolean isShowStatsGradeElevation(Context context) {
final boolean STATS_SHOW_GRADE_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_grade_elevation_default);
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_GRADE_ELEVATION);
public static boolean isShowStatsElevation(Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
}
public static boolean isStatsShowCoordinate(Context context) {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
android:fillColor="#ffffff"/>
</vector>
+5
View File
@@ -48,6 +48,11 @@ limitations under the License.
android:icon="@drawable/ic_delete_forever_24dp"
android:orderInCategory="10"
android:title="@string/menu_delete" />
<item
android:id="@+id/track_detail_resume_track"
android:icon="@drawable/ic_play_circle_24dp"
android:orderInCategory="10"
android:title="@string/menu_resume_track" />
<item
android:id="@+id/track_detail_settings"
android:icon="@drawable/ic_settings_24dp"
+2
View File
@@ -207,6 +207,7 @@
<string name="menu_stop_gps">Detener GPS</string>
<string name="menu_view_photo">Ver foto</string>
<string name="menu_voice_frequency">Frecuencia de voz</string>
<string name="menu_resume_track">Reanudar ruta</string>
<string name="app_name">OpenTracks</string>
<string name="recording_service">Servicio de grabación</string>
<string name="search_global_description">Recorridos y marcadores</string>
@@ -299,6 +300,7 @@
<string name="track_delete_progress_message">Eliminando…</string>
<string name="track_detail_chart_tab">Gráfico</string>
<string name="track_detail_stats_tab">Estadísticas</string>
<string name="track_detail_record_success">Grabando una nueva ruta</string>
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo)</string>
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
<string name="track_edit_new_track_title">Nuevo recorrido</string>
+2
View File
@@ -211,6 +211,7 @@
<string name="menu_stop_gps">Detener GPS</string>
<string name="menu_view_photo">Ver foto</string>
<string name="menu_voice_frequency">Frecuencia de voz</string>
<string name="menu_resume_track">Reanudar ruta</string>
<string name="app_name">OpenTracks</string>
<string name="recording_service">Servicio de grabación</string>
<string name="search_global_description">Recorridos y marcadores</string>
@@ -303,6 +304,7 @@
<string name="track_delete_progress_message">Eliminando…</string>
<string name="track_detail_chart_tab">Gráfico</string>
<string name="track_detail_stats_tab">Estadísticas</string>
<string name="track_detail_record_success">Grabando una nueva ruta</string>
<string name="track_edit_activity_type">Tipo de actividad (p. ej., senderismo…)</string>
<string name="track_edit_activity_type_hint">Tipo de actividad</string>
<string name="track_edit_new_track_title">Nueva pista</string>
+1 -1
View File
@@ -5,7 +5,7 @@
<bool name="stats_show_coordinate_default" translatable="false">false</bool>
<string name="stats_show_grade_elevation_key" translatable="false">statsShowGradeElevation</string>
<bool name="stats_show_grade_elevation_default" translatable="false">false</bool>
<bool name="stats_show_elevation_default" translatable="false">false</bool>
<string name="stats_show_on_lockscreen_while_recording_key" translatable="false">trackdetail_show_on_lockscreen_while_recording</string>
<bool name="stats_show_on_lockscreen_while_recording_default" translatable="false">false</bool>
+3
View File
@@ -321,6 +321,7 @@ limitations under the License.
<string name="menu_stop_gps">Stop GPS</string>
<string name="menu_view_photo">View photo</string>
<string name="menu_voice_frequency">Voice frequency</string>
<string name="menu_resume_track">Resume track</string>
<!-- OpenTracks -->
<string name="app_name">OpenTracks</string>
<string name="recording_service">Recording service</string>
@@ -440,6 +441,8 @@ limitations under the License.
<!-- Track Detail -->
<string name="track_detail_chart_tab">Chart</string>
<string name="track_detail_stats_tab">Stats</string>
<string name="track_detail_record_success">Now recording a new track</string>
<string name="track_detail_resume_success">Now resuming the track</string>
<!-- Track Edit -->
<string name="track_edit_activity_type">Activity type (e.g. hiking)</string>
<string name="track_edit_activity_type_hint">Activity type</string>
+1 -1
View File
@@ -44,7 +44,7 @@ limitations under the License.
<PreferenceCategory android:title="@string/settings_show">
<SwitchPreferenceCompat
android:defaultValue="@bool/stats_show_grade_elevation_default"
android:defaultValue="@bool/stats_show_elevation_default"
android:key="@string/stats_show_grade_elevation_key"
android:title="@string/stats_elevation" />
<SwitchPreferenceCompat