Extracted LocationIterator from ContentProviderUtils.

This commit is contained in:
Dennis Guse
2020-01-03 22:03:23 +01:00
parent 0e9b68c2a1
commit 8bc2937bb4
2 changed files with 141 additions and 135 deletions
@@ -22,7 +22,6 @@ import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -30,7 +29,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.android.ContentResolverWrapper;
@@ -831,102 +829,45 @@ 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 LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
* When done with iteration, {@link LocationIterator#close()} must be called.
* Fills a track point from a cursor.
*
* @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 locationFactory the location factory
* @param cursor the cursor pointing to a location.
* @param indexes the cached track points indexes
* @param location the track point
*/
public LocationIterator getTrackPointLocationIterator(final long trackId,
final long startTrackPointId, final boolean descending,
final LocationFactory locationFactory) {
if (locationFactory == null) {
throw new IllegalArgumentException("locationFactory is null");
static void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
location.reset();
if (!cursor.isNull(indexes.longitudeIndex)) {
location.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
}
return new LocationIterator() {
private long lastTrackPointId = -1L;
private Cursor cursor = getCursor(startTrackPointId);
private final CachedTrackPointsIndexes
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
: null;
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;
/**
* Gets the track point cursor.
*
* @param trackPointId the starting track point id
*/
private Cursor getCursor(long trackPointId) {
return getTrackPointCursor(trackId, trackPointId, defaultCursorBatchSize, descending);
}
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);
/**
* Advances the cursor to the next batch. Returns true if successful.
*/
private boolean advanceCursorToNextBatch() {
long trackPointId = lastTrackPointId == -1L ? -1L
: lastTrackPointId + (descending ? -1 : 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
}
@Override
public long getLocationId() {
return lastTrackPointId;
}
@Override
public boolean hasNext() {
if (cursor == null) {
return false;
}
if (cursor.isAfterLast()) {
return false;
}
if (cursor.isLast()) {
if (cursor.getCount() != defaultCursorBatchSize) {
return false;
}
return advanceCursorToNextBatch() && !cursor.isAfterLast();
}
return true;
}
@Override
public Location next() {
if (cursor == null) {
throw new NoSuchElementException();
}
if (!cursor.moveToNext()) {
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
Location location = locationFactory.createLocation();
fillTrackPoint(cursor, indexes, location);
return location;
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
sensorDataSetLocation.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
}
}
/**
@@ -984,45 +925,22 @@ public class ContentProviderUtils {
}
/**
* Fills a track point from a cursor.
* 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 LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
* When done with iteration, {@link LocationIterator#close()} must be called.
*
* @param cursor the cursor pointing to a location.
* @param indexes the cached track points indexes
* @param location the track point
* @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 locationFactory the location factory
*/
private 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()));
public LocationIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending, final LocationFactory locationFactory) {
if (locationFactory == null) {
throw new IllegalArgumentException("locationFactory is null");
}
return new LocationIterator(this, trackId, startTrackPointId, descending, locationFactory);
}
private Location findTrackPointBy(String selection, String[] selectionArgs) {
@@ -1046,6 +964,10 @@ public class ContentProviderUtils {
return contentResolver.query(TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
int getDefaultCursorBatchSize() {
return defaultCursorBatchSize;
}
/**
* Sets the default cursor batch size. For testing purpose.
*
@@ -1059,7 +981,7 @@ public class ContentProviderUtils {
/**
* A cache of track points indexes.
*/
private static class CachedTrackPointsIndexes {
static class CachedTrackPointsIndexes {
final int idIndex;
final int longitudeIndex;
final int latitudeIndex;
@@ -2,21 +2,105 @@ package de.dennisguse.opentracks.content;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
public class LocationIterator implements Iterator<Location>, AutoCloseable {
private static final String TAG = LocationIterator.class.getSimpleName();
private final ContentProviderUtils contentProviderUtils;
private final long trackId;
private final boolean descending;
private final LocationFactory locationFactory;
private final ContentProviderUtils.CachedTrackPointsIndexes indexes;
private long lastTrackPointId = -1L;
private Cursor cursor;
public LocationIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
this.descending = descending;
this.locationFactory = locationFactory;
cursor = getCursor(startTrackPointId);
indexes = cursor != null ? new ContentProviderUtils.CachedTrackPointsIndexes(cursor)
: null;
}
/**
* Gets the most recently retrieved track point id by {@link #next()}.
* Gets the track point cursor.
*
* @param trackPointId the starting track point id
*/
long getLocationId();
private Cursor getCursor(long trackPointId) {
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize(), descending);
}
/**
* Closes the iterator.
* Advances the cursor to the next batch. Returns true if successful.
*/
void close();
private boolean advanceCursorToNextBatch() {
long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 : 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
}
public long getLocationId() {
return lastTrackPointId;
}
@Override
public boolean hasNext() {
if (cursor == null) {
return false;
}
if (cursor.isAfterLast()) {
return false;
}
if (cursor.isLast()) {
if (cursor.getCount() != contentProviderUtils.getDefaultCursorBatchSize()) {
return false;
}
return advanceCursorToNextBatch() && !cursor.isAfterLast();
}
return true;
}
@Override
public Location next() {
if (cursor == null) {
throw new NoSuchElementException();
}
if (!cursor.moveToNext()) {
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
Location location = locationFactory.createLocation();
ContentProviderUtils.fillTrackPoint(cursor, indexes, location);
return location;
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}