From 221b61c7f1e0e5dfe5feb193f68c687e8f45792e Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Wed, 5 Nov 2025 20:10:42 +0100 Subject: [PATCH] Aggregate statistics are now computed in the database. --- .../AggregatedStatisticsTest.java | 248 +++++++----------- .../opentracks/data/AggregatedStatistic.java | 26 ++ .../opentracks/data/ContentProviderUtils.java | 38 +-- .../data/CustomContentProvider.java | 26 +- .../opentracks/data/models/Distance.java | 2 + .../opentracks/data/models/Speed.java | 1 + .../opentracks/data/models/Statistics.java | 3 - .../opentracks/data/tables/TracksColumns.java | 3 + .../AggregatedStatistics.java | 81 ------ .../AggregatedStatisticsActivity.java | 10 +- .../AggregatedStatisticsAdapter.java | 51 ++-- .../AggregatedStatisticsModel.java | 12 +- 12 files changed, 197 insertions(+), 304 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/data/AggregatedStatistic.java delete mode 100644 src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatistics.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsTest.java index e6a76dcef..934528cd2 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsTest.java @@ -1,12 +1,12 @@ package de.dennisguse.opentracks.ui.aggregatedStatistics; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import android.content.Context; import androidx.test.core.app.ApplicationProvider; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -16,6 +16,9 @@ import java.time.Instant; import java.util.List; import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.AggregatedStatistic; +import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.TrackSelection; import de.dennisguse.opentracks.data.models.ActivityType; import de.dennisguse.opentracks.data.models.AltitudeExtremities; import de.dennisguse.opentracks.data.models.AltitudeGainLoss; @@ -29,7 +32,7 @@ public class AggregatedStatisticsTest { private final Context context = ApplicationProvider.getApplicationContext(); - private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String activityTypeLocalized) { + private static void createTrack(Context context, Distance totalDistance, Duration totalTime, String activityTypeLocalized) { Track track = new Track(new Statistics( Instant.ofEpochMilli(1000), Instant.ofEpochMilli(1000).plus(totalTime), @@ -44,199 +47,128 @@ public class AggregatedStatisticsTest { )); track.setActivityType(ActivityType.findByLocalizedString(context, activityTypeLocalized)); track.setActivityTypeLocalized(activityTypeLocalized); - return track; + + new ContentProviderUtils(context).insertTrack(track); + } + + @Before + public void setUp() { + new ContentProviderUtils(context).deleteAllTracks(context); } @Test - public void testAggregate() { + public void testAggregate_empty() { + // when + List result = new ContentProviderUtils(context).getAggregatedStatisticsForTracks(new TrackSelection()); + + // then + assertEquals(List.of(), result); + } + + @Test + public void testAggregate_oneBikingTrack() { // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - Duration totalTime = Duration.ofMillis(2400000); + Distance totalDistance = Distance.ofKilometer(10); + Duration totalTime = Duration.ofMinutes(40); String biking = context.getString(R.string.activity_type_biking); - Track track = createTrack(context, totalDistance, totalTime, biking); + + createTrack(context, totalDistance, totalTime, biking); // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track)); + List result = new ContentProviderUtils(context).getAggregatedStatisticsForTracks(new TrackSelection()); // then - assertEquals(1, aggregatedStatistics.getCount()); - assertNotNull(aggregatedStatistics.get(biking)); - assertEquals(1, aggregatedStatistics.get(biking).getCountTracks()); - - assertEquals(totalDistance, aggregatedStatistics.get(biking).totalDistance); - assertEquals(totalTime, aggregatedStatistics.get(biking).totalMovingTime); - } - - @Test - public void testAggregate_mountainBiking() { - // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - Duration totalTime = Duration.ofMillis(2400000); - String mountainBiking = context.getString(R.string.activity_type_mountain_biking); - Track track = createTrack(context, totalDistance, totalTime, mountainBiking); - - // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track)); - - // then - assertNotNull(aggregatedStatistics.get(mountainBiking)); - } - - @Test - public void testAggregate_trailRunning() { - // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - Duration totalTime = Duration.ofMillis(2400000); - String trailRunning = context.getString(R.string.activity_type_trail_running); - Track track = createTrack(context, totalDistance, totalTime, trailRunning); - - // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track)); - - // then - assertNotNull(aggregatedStatistics.get(trailRunning)); + assertEquals(List.of( + new AggregatedStatistic("biking", 1, Duration.ofMinutes(40), Distance.ofKilometer(10), Speed.of(50)) + ), result); } @Test public void testAggregate_twoBikingTracks() { // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - Duration totalTime = Duration.ofMillis(2400000); + Distance totalDistance = Distance.ofKilometer(10); + Duration totalTime = Duration.ofMinutes(40); String biking = context.getString(R.string.activity_type_biking); - List tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking)); + + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, biking); // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks); + List result = new ContentProviderUtils(context).getAggregatedStatisticsForTracks(new TrackSelection()); // then - assertEquals(1, aggregatedStatistics.getCount()); - assertNotNull(aggregatedStatistics.get(biking)); - assertEquals(2, aggregatedStatistics.get(biking).getCountTracks()); - - assertEquals(totalDistance.multipliedBy(2), aggregatedStatistics.get(biking).totalDistance); - assertEquals(totalTime.multipliedBy(2), aggregatedStatistics.get(biking).totalMovingTime); - } - - @Test - public void testAggregate_threeDifferentTracks() { - // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - - String biking = context.getString(R.string.activity_type_biking); - String running = context.getString(R.string.activity_type_running); - String walking = context.getString(R.string.activity_type_walking); - Duration totalTime = Duration.ofMillis(2400000); - List tracks = List.of( - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, running), - createTrack(context, totalDistance, totalTime, walking) - ); - - // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks); - - // then - assertEquals(3, aggregatedStatistics.getCount()); - assertNotNull(aggregatedStatistics.get(biking)); - assertNotNull(aggregatedStatistics.get(running)); - assertNotNull(aggregatedStatistics.get(walking)); - assertEquals(1, aggregatedStatistics.get(biking).getCountTracks()); - assertEquals(1, aggregatedStatistics.get(running).getCountTracks()); - assertEquals(1, aggregatedStatistics.get(walking).getCountTracks()); - - { - assertEquals(totalDistance, aggregatedStatistics.get(biking).totalDistance); - assertEquals(totalTime, aggregatedStatistics.get(biking).totalMovingTime); - } - - { - assertEquals(totalDistance, aggregatedStatistics.get(running).totalDistance); - assertEquals(totalTime, aggregatedStatistics.get(running).totalMovingTime); - } - - { - assertEquals(totalDistance, aggregatedStatistics.get(walking).totalDistance); - assertEquals(totalTime, aggregatedStatistics.get(walking).totalMovingTime); - } + assertEquals(List.of( + new AggregatedStatistic("biking", 2, Duration.ofMinutes(80), Distance.ofKilometer(20), Speed.of(50)) + ), result); } @Test public void testAggregate_severalTracksWithSeveralActivities() { // given - // 10km in 40 minutes. - Distance totalDistance = Distance.of(10000); - Duration totalTime = Duration.ofMillis(2400000); + Distance totalDistance = Distance.ofKilometer(10); + Duration totalTime = Duration.ofMinutes(40); + String biking = context.getString(R.string.activity_type_biking); String running = context.getString(R.string.activity_type_running); String walking = context.getString(R.string.activity_type_walking); String driving = context.getString(R.string.activity_type_driving); - List tracks = List.of( - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, running), - createTrack(context, totalDistance, totalTime, walking), - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, running), - createTrack(context, totalDistance, totalTime, walking), - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, biking), - createTrack(context, totalDistance, totalTime, driving) - ); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, running); + createTrack(context, totalDistance, totalTime, walking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, running); + createTrack(context, totalDistance, totalTime, walking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, driving); // when - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks); + List result = new ContentProviderUtils(context).getAggregatedStatisticsForTracks(new TrackSelection()); // then - // 4 sports. - assertEquals(4, aggregatedStatistics.getCount()); + assertEquals(List.of( + new AggregatedStatistic("biking", 5, Duration.ofMinutes(200), Distance.ofKilometer(50), Speed.of(50)), + new AggregatedStatistic("walking", 2, Duration.ofMinutes(80), Distance.ofKilometer(20), Speed.of(50)), + new AggregatedStatistic("running", 2, Duration.ofMinutes(80), Distance.ofKilometer(20), Speed.of(50)), + new AggregatedStatistic("driving", 1, Duration.ofMinutes(40), Distance.ofKilometer(10), Speed.of(50)) + ), result); + } + + @Test + public void testAggregate_severalTracksWithSeveralActivities_TrackSelection() { + // given + Distance totalDistance = Distance.ofKilometer(10); + Duration totalTime = Duration.ofMinutes(40); - // There is a map for every sport. - assertNotNull(aggregatedStatistics.get(biking)); - assertNotNull(aggregatedStatistics.get(running)); - assertNotNull(aggregatedStatistics.get(walking)); - assertNotNull(aggregatedStatistics.get(driving)); + String biking = context.getString(R.string.activity_type_biking); + String running = context.getString(R.string.activity_type_running); + String walking = context.getString(R.string.activity_type_walking); + String driving = context.getString(R.string.activity_type_driving); - // Number of tracks by sport. - assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking. - assertEquals(2, aggregatedStatistics.get(running).getCountTracks()); // Running. - assertEquals(2, aggregatedStatistics.get(walking).getCountTracks()); // Walking. - assertEquals(1, aggregatedStatistics.get(driving).getCountTracks()); // Driving. + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, running); + createTrack(context, totalDistance, totalTime, walking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, running); + createTrack(context, totalDistance, totalTime, walking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, biking); + createTrack(context, totalDistance, totalTime, driving); - // Biking. - { - assertEquals(totalDistance.multipliedBy(5), aggregatedStatistics.get(biking).totalDistance); - assertEquals(totalTime.multipliedBy(5), aggregatedStatistics.get(biking).totalMovingTime); - } + TrackSelection trackSelection = new TrackSelection(); + trackSelection.addActivityType("biking"); + trackSelection.addActivityType("driving"); - // Running. - { - assertEquals(totalDistance.multipliedBy(2), aggregatedStatistics.get(running).totalDistance); - assertEquals(totalTime.multipliedBy(2), aggregatedStatistics.get(running).totalMovingTime); - } + // when + List result = new ContentProviderUtils(context).getAggregatedStatisticsForTracks(trackSelection); - // Walking. - { - assertEquals(totalDistance.multipliedBy(2), aggregatedStatistics.get(walking).totalDistance); - assertEquals(totalTime.multipliedBy(2), aggregatedStatistics.get(walking).totalMovingTime); - } - - // Driving. - { - assertEquals(totalDistance, aggregatedStatistics.get(driving).totalDistance); - assertEquals(totalTime, aggregatedStatistics.get(driving).totalMovingTime); - } - - // Check order - - { - assertEquals(biking, aggregatedStatistics.getItem(0).getActivityTypeLocalized()); - assertEquals(driving, aggregatedStatistics.getItem(3).getActivityTypeLocalized()); - } + // then + assertEquals(List.of( + new AggregatedStatistic("biking", 5, Duration.ofMinutes(200), Distance.ofKilometer(50), Speed.of(50)), + new AggregatedStatistic("driving", 1, Duration.ofMinutes(40), Distance.ofKilometer(10), Speed.of(50)) + ), result); } } diff --git a/src/main/java/de/dennisguse/opentracks/data/AggregatedStatistic.java b/src/main/java/de/dennisguse/opentracks/data/AggregatedStatistic.java new file mode 100644 index 000000000..9a32ca15f --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/data/AggregatedStatistic.java @@ -0,0 +1,26 @@ +package de.dennisguse.opentracks.data; + +import androidx.annotation.NonNull; + +import java.time.Duration; + +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Speed; + +public record AggregatedStatistic( + @NonNull + String activityTypeLocalized, + int countTracks, + + @NonNull + Duration totalMovingTime, + @NonNull + Distance totalDistance, + @NonNull + Speed maxSpeed +) { + + public Speed getAverageMovingSpeed() { + return Speed.of(totalDistance, totalMovingTime); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java index dac29c9a6..8204efc80 100644 --- a/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/data/ContentProviderUtils.java @@ -66,6 +66,7 @@ import de.dennisguse.opentracks.util.FileUtils; * * @author Leif Hendrik Wilden */ +//TODO Check if do {} while can be replaced by while (cursor.moveToNext) public class ContentProviderUtils { private static final String TAG = ContentProviderUtils.class.getSimpleName(); @@ -80,6 +81,7 @@ public class ContentProviderUtils { private final ContentResolver contentResolver; + //TODO Check if this can be used in more than one place. If not remove. public interface ContentProviderSelectionInterface { SelectionData buildSelection(); } @@ -200,21 +202,7 @@ public class ContentProviderUtils { return tracks; } - public List getTracks(ContentProviderSelectionInterface selection) { - SelectionData selectionData = selection.buildSelection(); - ArrayList tracks = new ArrayList<>(); - try (Cursor cursor = getTrackCursor(selectionData.selection(), selectionData.selectionArgs(), TracksColumns._ID)) { - if (cursor != null && cursor.moveToFirst()) { - tracks.ensureCapacity(cursor.getCount()); - do { - tracks.add(createTrack(cursor)); - } while (cursor.moveToNext()); - } - } - - return tracks; - } - + //TODO Implement someting like TrackPointIterator: just as a cursor public Cursor searchTracks(String searchQuery) { // Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested. // Used only be TrackListAdapter @@ -311,6 +299,8 @@ public class ContentProviderUtils { values.put(TracksColumns.ACTIVITY_TYPE, track.getActivityType() != null ? track.getActivityType().getId() : null); values.put(TracksColumns.ACTIVITY_TYPE_LOCALIZED, track.getActivityTypeLocalized()); values.put(TracksColumns.STARTTIME_OFFSET, track.getZoneOffset().getTotalSeconds()); + + //TODO Duplicates createContentValues(Statistics trackStatistics) if (trackStatistics.startTime() != null) { values.put(TracksColumns.STARTTIME, trackStatistics.startTime().toEpochMilli()); } @@ -897,4 +887,22 @@ public class ContentProviderUtils { } return sensorStatistics; } + + @NonNull + public List getAggregatedStatisticsForTracks(ContentProviderSelectionInterface selection) { + SelectionData data = selection.buildSelection(); + try (Cursor cursor = contentResolver.query(TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS, null, data.selection(), data.selectionArgs(), null)) { + ArrayList aggregatedStatistics = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + aggregatedStatistics.add(new AggregatedStatistic( + cursor.getString(cursor.getColumnIndexOrThrow(TracksColumns.ACTIVITY_TYPE_LOCALIZED)), + cursor.getInt(cursor.getColumnIndexOrThrow(TracksColumns.TRACK_COUNT)), + Duration.ofMillis(cursor.getLong(cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME))), + Distance.of(cursor.getFloat(cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE))), + Speed.of(cursor.getFloat(cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED))) + )); + } + return aggregatedStatistics; + } + } } diff --git a/src/main/java/de/dennisguse/opentracks/data/CustomContentProvider.java b/src/main/java/de/dennisguse/opentracks/data/CustomContentProvider.java index e63413534..9d3c77935 100644 --- a/src/main/java/de/dennisguse/opentracks/data/CustomContentProvider.java +++ b/src/main/java/de/dennisguse/opentracks/data/CustomContentProvider.java @@ -30,7 +30,6 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.util.Arrays; @@ -65,7 +64,7 @@ public class CustomContentProvider extends ContentProvider { * Finally, it ignores manual pause (SEGMENT_START_MANUAL). */ private final String SENSOR_STATS_QUERY = - "WITH time_select as " + + "WITH time_select AS " + "(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " + "FROM " + TrackPointsColumns.TABLE_NAME + " t1 " + "WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " + @@ -101,6 +100,7 @@ public class CustomContentProvider extends ContentProvider { uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal()); + uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS.getPath(), UrlType.TRACKS_AGGREGATED_STATISTICS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal()); @@ -223,6 +223,8 @@ public class CustomContentProvider extends ContentProvider { public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sort) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); String sortOrder = null; + String groupBy = null; + //TODO queryBuilder.appendWhere() should use selectionArgs? switch (getUrlType(url)) { case TRACKPOINTS -> { queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); @@ -249,9 +251,23 @@ public class CustomContentProvider extends ContentProvider { queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); } case TRACKS_SENSOR_STATS -> { + //TODO Replace with SQLQueryBuilder? long trackId = ContentUris.parseId(url); return db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)}); } + case TRACKS_AGGREGATED_STATISTICS -> { + queryBuilder.setTables(TracksColumns.TABLE_NAME); + projection = new String[]{ + "COUNT(*) AS " + TracksColumns.TRACK_COUNT, + TracksColumns.ACTIVITY_TYPE_LOCALIZED, + "SUM(" + TracksColumns.MOVINGTIME + ") AS " + TracksColumns.MOVINGTIME, + "SUM(" + TracksColumns.TOTALDISTANCE + ") AS " + TracksColumns.TOTALDISTANCE, + "MAX(" + TracksColumns.MAXSPEED + ") AS " + TracksColumns.MAXSPEED + }; + + sortOrder = TracksColumns.TRACK_COUNT + " DESC"; + groupBy = TracksColumns.ACTIVITY_TYPE_LOCALIZED; + } case MARKERS -> { queryBuilder.setTables(MarkerColumns.TABLE_NAME); sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER; @@ -266,7 +282,7 @@ public class CustomContentProvider extends ContentProvider { } default -> throw new IllegalArgumentException("Unknown url " + url); } - Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); + Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), url); return cursor; } @@ -379,14 +395,14 @@ public class CustomContentProvider extends ContentProvider { throw new SQLException("Failed to insert a marker " + url); } - @VisibleForTesting - enum UrlType { + private enum UrlType { TRACKPOINTS, TRACKPOINTS_BY_ID, TRACKPOINTS_BY_TRACKID, TRACKS, TRACKS_BY_ID, TRACKS_SENSOR_STATS, + TRACKS_AGGREGATED_STATISTICS, MARKERS, MARKERS_BY_ID, MARKERS_BY_TRACKID diff --git a/src/main/java/de/dennisguse/opentracks/data/models/Distance.java b/src/main/java/de/dennisguse/opentracks/data/models/Distance.java index 3dc6ae3c6..ce6a3352b 100644 --- a/src/main/java/de/dennisguse/opentracks/data/models/Distance.java +++ b/src/main/java/de/dennisguse/opentracks/data/models/Distance.java @@ -19,6 +19,8 @@ public record Distance(double distance_m) { return Distance.of((double) distance_m); } } + + @Deprecated //TODO Remove public static Distance of(String distance_m) { return of(Float.parseFloat(distance_m)); } diff --git a/src/main/java/de/dennisguse/opentracks/data/models/Speed.java b/src/main/java/de/dennisguse/opentracks/data/models/Speed.java index 19a8c36fc..a0ab63b7d 100644 --- a/src/main/java/de/dennisguse/opentracks/data/models/Speed.java +++ b/src/main/java/de/dennisguse/opentracks/data/models/Speed.java @@ -20,6 +20,7 @@ public record Speed(double speed_mps) { return new Speed(speed_mps); } + @Deprecated //TODO Remove public static Speed of(String speed_mps) { return of(Float.parseFloat(speed_mps)); } diff --git a/src/main/java/de/dennisguse/opentracks/data/models/Statistics.java b/src/main/java/de/dennisguse/opentracks/data/models/Statistics.java index 458f0d578..77d4b64c4 100644 --- a/src/main/java/de/dennisguse/opentracks/data/models/Statistics.java +++ b/src/main/java/de/dennisguse/opentracks/data/models/Statistics.java @@ -35,9 +35,6 @@ public record Statistics( } public Speed getAverageSpeed() { - if (totalTime.isZero()) { - return Speed.ZERO; - } return Speed.of(totalDistance, totalTime); } diff --git a/src/main/java/de/dennisguse/opentracks/data/tables/TracksColumns.java b/src/main/java/de/dennisguse/opentracks/data/tables/TracksColumns.java index 4aa37f299..13a281da9 100644 --- a/src/main/java/de/dennisguse/opentracks/data/tables/TracksColumns.java +++ b/src/main/java/de/dennisguse/opentracks/data/tables/TracksColumns.java @@ -31,6 +31,7 @@ public interface TracksColumns extends BaseColumns { String TABLE_NAME = "tracks"; Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME); Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats"); + Uri CONTENT_URI_AGGREGATED_STATISTICS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/aggregated/"); String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track"; String DEFAULT_SORT_ORDER = _ID; @@ -46,7 +47,9 @@ public interface TracksColumns extends BaseColumns { String STARTTIME_OFFSET = "time_offset"; // in plus/minus in seconds String STARTTIME = "time_start"; // track start time String STOPTIME = "time_stop"; // track stop time + String MARKER_COUNT = "markerCount"; // the numbers of markers (virtual column) + String TRACK_COUNT = "trackCount"; // the numbers of markers (virtual column) String TOTALDISTANCE = "distance"; // total distance String TOTALTIME = "duration_total"; diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatistics.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatistics.java deleted file mode 100644 index 5c69e3fc7..000000000 --- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatistics.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.dennisguse.opentracks.ui.aggregatedStatistics; - -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.Speed; -import de.dennisguse.opentracks.data.models.Track; - -public class AggregatedStatistics { - - private final List dataList; - - public AggregatedStatistics(@NonNull List tracks) { - Map> groupedByActivityType = tracks.stream().collect(Collectors.groupingBy(Track::getActivityTypeLocalized)); - - List>> groupedByActivityTypeList = new ArrayList<>(); - groupedByActivityType.forEach((activityTypeLocalized, trackList) -> groupedByActivityTypeList.add(new Pair<>(activityTypeLocalized, trackList))); - - dataList = groupedByActivityTypeList.parallelStream() - .map(i -> new AggregatedStatistic(i.first, i.second)) - .sorted((o1, o2) -> { - if (o1.getCountTracks() == o2.getCountTracks()) { - return o1.getActivityTypeLocalized().compareTo(o2.getActivityTypeLocalized()); - } - return (o1.getCountTracks() < o2.getCountTracks() ? 1 : -1); - }).toList(); - } - - public int getCount() { - return dataList.size(); - } - - @VisibleForTesting - public AggregatedStatistic get(String activityTypeLocalized) { - return dataList.stream().filter(i -> i.getActivityTypeLocalized().equals(activityTypeLocalized)).findFirst().get(); - } - - public AggregatedStatistic getItem(int position) { - return dataList.get(position); - } - - public static class AggregatedStatistic { - final String activityTypeLocalized; - final int countTracks; - - final Duration totalMovingTime; - - final Distance totalDistance; - - final Speed avgMovingSpeed; - - final Speed maxSpeed; - - public AggregatedStatistic(String activityTypeLocalized, List tracks) { - this.activityTypeLocalized = activityTypeLocalized; - this.countTracks = tracks.size(); - - totalMovingTime = tracks.stream().map(t -> t.getStatistics().movingTime()).reduce(Duration.ZERO, Duration::plus); - totalDistance = tracks.stream().map(t -> t.getStatistics().totalDistance()).reduce(Distance.ZERO, Distance::plus); - maxSpeed = tracks.stream().map(t -> t.getStatistics().maxSpeed()).max((o1, o2) -> (int) (o1.speed_mps() - o2.speed_mps())).orElse(Speed.ZERO); - avgMovingSpeed = Speed.of(totalDistance, totalMovingTime); - } - - public String getActivityTypeLocalized() { - return activityTypeLocalized; - } - - public int getCountTracks() { - return countTracks; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsActivity.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsActivity.java index 596fda6e0..7fdc07a3a 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsActivity.java @@ -55,12 +55,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class); viewModel.getAggregatedStats(selection).observe(this, aggregatedStatistics -> { - if ((aggregatedStatistics == null || aggregatedStatistics.getCount() == 0) && !selection.isEmpty()) { - viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results)); - } - if (aggregatedStatistics != null) { - adapter.swapData(aggregatedStatistics); - } + adapter.swapData(aggregatedStatistics); checkListEmpty(); }); @@ -71,6 +66,9 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi if (adapter.getItemCount() == 0) { viewBinding.aggregatedStatsList.setVisibility(View.GONE); viewBinding.aggregatedStatsEmptyView.setVisibility(View.VISIBLE); + if (!selection.isEmpty()) { + viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results)); + } } else { viewBinding.aggregatedStatsList.setVisibility(View.VISIBLE); viewBinding.aggregatedStatsEmptyView.setVisibility(View.GONE); diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java index 491b097fe..5760b8b44 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsAdapter.java @@ -8,10 +8,10 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; import java.util.List; import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.data.AggregatedStatistic; import de.dennisguse.opentracks.data.models.ActivityType; import de.dennisguse.opentracks.data.models.DistanceFormatter; import de.dennisguse.opentracks.data.models.SpeedFormatter; @@ -22,10 +22,10 @@ import de.dennisguse.opentracks.util.StringUtils; public class AggregatedStatisticsAdapter extends RecyclerView.Adapter { - private AggregatedStatistics aggregatedStatistics; + private List aggregatedStatistics; private final Context context; - public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) { + public AggregatedStatisticsAdapter(Context context, @NonNull List aggregatedStatistics) { this.context = context; this.aggregatedStatistics = aggregatedStatistics; } @@ -40,9 +40,9 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter aggregatedStatistics) { this.aggregatedStatistics = aggregatedStatistics; this.notifyDataSetChanged(); } public List getCategories() { - List categories = new ArrayList<>(); - for (int i = 0; i < aggregatedStatistics.getCount(); i++) { - categories.add(aggregatedStatistics.getItem(i).getActivityTypeLocalized()); - } - return categories; + return aggregatedStatistics.stream() + .map(AggregatedStatistic::activityTypeLocalized) + .toList(); } private class ViewHolder extends RecyclerView.ViewHolder { @@ -81,38 +76,38 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter parts = formatter.getSpeedParts(aggregatedStatistic.avgMovingSpeed); + Pair parts = formatter.getSpeedParts(aggregatedStatistic.getAverageMovingSpeed()); viewBinding.aggregatedStatsAvgRate.setText(parts.first); viewBinding.aggregatedStatsAvgRateUnit.setText(parts.second); viewBinding.aggregatedStatsAvgRateLabel.setText(context.getString(R.string.stats_average_moving_speed)); } { - Pair parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed); + Pair parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed()); viewBinding.aggregatedStatsMaxRate.setText(parts.first); viewBinding.aggregatedStatsMaxRateUnit.setText(parts.second); viewBinding.aggregatedStatsMaxRateLabel.setText(context.getString(R.string.stats_max_speed)); } } - public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) { + public void setPace(AggregatedStatistic aggregatedStatistic) { setCommonValues(aggregatedStatistic); SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context); { - Pair parts = formatter.getSpeedParts(aggregatedStatistic.avgMovingSpeed); + Pair parts = formatter.getSpeedParts(aggregatedStatistic.getAverageMovingSpeed()); viewBinding.aggregatedStatsAvgRate.setText(parts.first); viewBinding.aggregatedStatsAvgRateUnit.setText(parts.second); viewBinding.aggregatedStatsAvgRateLabel.setText(context.getString(R.string.stats_average_moving_pace)); } { - Pair parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed); + Pair parts = formatter.getSpeedParts(aggregatedStatistic.maxSpeed()); viewBinding.aggregatedStatsMaxRate.setText(parts.first); viewBinding.aggregatedStatsMaxRateUnit.setText(parts.second); viewBinding.aggregatedStatsMaxRateLabel.setText(R.string.stats_fastest_pace); @@ -120,27 +115,27 @@ public class AggregatedStatisticsAdapter extends RecyclerView.Adapter parts = DistanceFormatter.Builder() .setUnit(unitSystem) - .build(context).getDistanceParts(aggregatedStatistic.totalDistance); + .build(context).getDistanceParts(aggregatedStatistic.totalDistance()); viewBinding.aggregatedStatsDistance.setText(parts.first); viewBinding.aggregatedStatsDistanceUnit.setText(parts.second); - viewBinding.aggregatedStatsTime.setText(StringUtils.formatElapsedTime(aggregatedStatistic.totalMovingTime)); + viewBinding.aggregatedStatsTime.setText(StringUtils.formatElapsedTime(aggregatedStatistic.totalMovingTime())); } - private int getIcon(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) { - String localizedActivityType = aggregatedStatistic.getActivityTypeLocalized(); + private int getIcon(AggregatedStatistic aggregatedStatistic) { + String localizedActivityType = aggregatedStatistic.activityTypeLocalized(); return ActivityType.findByLocalizedString(context, localizedActivityType) .getIconDrawableId(); } diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsModel.java index 5115a00c5..3e1cc138a 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsModel.java +++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/AggregatedStatisticsModel.java @@ -10,19 +10,19 @@ import androidx.lifecycle.MutableLiveData; import java.util.List; +import de.dennisguse.opentracks.data.AggregatedStatistic; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.TrackSelection; -import de.dennisguse.opentracks.data.models.Track; public class AggregatedStatisticsModel extends AndroidViewModel { - private MutableLiveData aggregatedStats; + private MutableLiveData> aggregatedStats; public AggregatedStatisticsModel(@NonNull Application application) { super(application); } - public LiveData getAggregatedStats(@Nullable TrackSelection selection) { + public LiveData> getAggregatedStats(@Nullable TrackSelection selection) { if (aggregatedStats == null) { aggregatedStats = new MutableLiveData<>(); loadAggregatedStats(selection); @@ -41,11 +41,7 @@ public class AggregatedStatisticsModel extends AndroidViewModel { private void loadAggregatedStats(TrackSelection selection) { new Thread(() -> { ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext()); - List tracks = selection != null ? contentProviderUtils.getTracks(selection) : contentProviderUtils.getTracks(); - - AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks); - - aggregatedStats.postValue(aggregatedStatistics); + aggregatedStats.postValue(contentProviderUtils.getAggregatedStatisticsForTracks(selection)); }).start(); } }