From bae3373fe9546ce6fab88ee39d58433810dec5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n?= Date: Sun, 7 Jun 2020 12:01:22 +0200 Subject: [PATCH] Aggregated statistics by activity type. Fixes #112. --- .idea/misc.xml | 2 +- .../viewmodels/AggregatedStatisticsTest.java | 257 ++++++++++++++++++ src/main/AndroidManifest.xml | 5 + .../AggregatedStatisticsActivity.java | 41 +++ .../opentracks/TrackListActivity.java | 4 + .../adapters/AggregatedStatisticsAdapter.java | 181 ++++++++++++ .../opentracks/util/StringUtils.java | 4 + .../viewmodels/AggregatedStatistics.java | 74 +++++ .../viewmodels/AggregatedStatisticsModel.java | 42 +++ src/main/res/drawable/ic_statistics_24dp.xml | 5 + src/main/res/layout/aggregated_stats.xml | 14 + .../res/layout/aggregated_stats_list_item.xml | 150 ++++++++++ src/main/res/menu/track_list.xml | 5 + src/main/res/values/styles.xml | 7 + 14 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java create mode 100644 src/main/java/de/dennisguse/opentracks/AggregatedStatisticsActivity.java create mode 100644 src/main/java/de/dennisguse/opentracks/adapters/AggregatedStatisticsAdapter.java create mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatistics.java create mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsModel.java create mode 100644 src/main/res/drawable/ic_statistics_24dp.xml create mode 100644 src/main/res/layout/aggregated_stats.xml create mode 100644 src/main/res/layout/aggregated_stats_list_item.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a750962..7bfef59df 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java new file mode 100644 index 000000000..ad6ad369e --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java @@ -0,0 +1,257 @@ +package de.dennisguse.opentracks.viewmodels; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.Track; +import de.dennisguse.opentracks.stats.TrackStatistics; +import de.dennisguse.opentracks.util.TrackIconUtils; + +@RunWith(JUnit4.class) +public class AggregatedStatisticsTest { + + private final Context context = ApplicationProvider.getApplicationContext(); + + /** + * Create a TrackStatistics object. + * + * @param totalDistance distance in meters. + * @param totalTime total time in milliseconds. + * @return TrackStatistics object. + */ + private static Track createTrack(Context context, long totalDistance, long totalTime, String category) { + TrackStatistics statistics = new TrackStatistics(); + statistics.setStartTime_ms(1000L); // Resulting start time + statistics.setStopTime_ms(1000L + totalTime); + statistics.setTotalTime(totalTime); + statistics.setMovingTime(totalTime); + statistics.setTotalDistance(totalDistance); + statistics.setTotalElevationGain(50.0); + statistics.setMaxSpeed(50.0); // Resulting max speed + statistics.setMaxElevation(1250.0); + statistics.setMinElevation(1200.0); // Resulting min elevation + + Track track = new Track(); + track.setIcon(TrackIconUtils.getIconValue(context, category)); + track.setCategory(category); + track.setTrackStatistics(statistics); + return track; + } + + @Test + public void testAggregate() { + // given + // 10km in 40 minutes. + long totalDistance = 10000; + long totalTime = 2400000; + String biking = context.getString(R.string.activity_type_biking); + Track track = createTrack(context, totalDistance, totalTime, biking); + + // when + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(track); + + // then + Assert.assertEquals(1, aggregatedStatistics.getCount()); + Assert.assertTrue(aggregatedStatistics.get(biking) != null); + Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks()); + + TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); + Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime, statistics2.getMovingTime()); + } + + @Test + public void testAggregate_mountainBiking() { + // given + // 10km in 40 minutes. + long totalDistance = 10000; + long totalTime = 2400000; + String mountainBiking = context.getString(R.string.activity_type_mountain_biking); + Track track = createTrack(context, totalDistance, totalTime, mountainBiking); + + // when + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(track); + + // then + Assert.assertTrue(aggregatedStatistics.get(mountainBiking) != null); + } + + @Test + public void testAggregate_trailRunning() { + // given + // 10km in 40 minutes. + long totalDistance = 10000; + long totalTime = 2400000; + String trailRunning = context.getString(R.string.activity_type_trail_running); + Track track = createTrack(context, totalDistance, totalTime, trailRunning); + + // when + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(track); + + // then + Assert.assertTrue(aggregatedStatistics.get(trailRunning) != null); + } + + @Test + public void testAggregate_twoBikingTracks() { + // given + // 10km in 40 minutes. + long totalDistance = 10000; + long totalTime = 2400000; + String biking = context.getString(R.string.activity_type_biking); + Track[] tracks = new Track[2]; + for (int i = 0; i < 2; i++) { + tracks[i] = createTrack(context, totalDistance, totalTime, biking); + } + + // when + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(tracks[0]); // biking activity 1. + aggregatedStatistics.aggregate(tracks[1]); // biking activity 2. + + // then + Assert.assertEquals(1, aggregatedStatistics.getCount()); + Assert.assertTrue(aggregatedStatistics.get(biking) != null); + Assert.assertEquals(2, aggregatedStatistics.get(biking).getCountTracks()); + + TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); + Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime * 2L, statistics2.getMovingTime()); + } + + @Test + public void testAggregate_threeDifferentTracks() { + // given + // 10km in 40 minutes. + long totalDistance = 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); + long totalTime = 2400000; + Track[] tracks = new Track[]{ + createTrack(context, totalDistance, totalTime, biking), + createTrack(context, totalDistance, totalTime, running), + createTrack(context, totalDistance, totalTime, walking) + }; + + // when + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(tracks[0]); // biking activity 1. + aggregatedStatistics.aggregate(tracks[1]); // biking activity 2. + aggregatedStatistics.aggregate(tracks[2]); // biking activity 3. + + // then + Assert.assertEquals(3, aggregatedStatistics.getCount()); + Assert.assertTrue(aggregatedStatistics.get(biking) != null); + Assert.assertTrue(aggregatedStatistics.get(running) != null); + Assert.assertTrue(aggregatedStatistics.get(walking) != null); + Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks()); + Assert.assertEquals(1, aggregatedStatistics.get(running).getCountTracks()); + Assert.assertEquals(1, aggregatedStatistics.get(walking).getCountTracks()); + + { + TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); + Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime, statistics2.getMovingTime()); + } + + { + TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics(); + Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime, statistics2.getMovingTime()); + } + + { + TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics(); + Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime, statistics2.getMovingTime()); + } + } + + @Test + public void testAggregate_severalTracksWithSeveralActivities() { + // given + // 10km in 40 minutes. + long totalDistance = 10000; + long totalTime = 2400000; + 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); + Track[] tracks = new Track[]{ + 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(); + for (Track track : tracks) { + aggregatedStatistics.aggregate(track); + + } + + // then + // 4 sports. + Assert.assertEquals(4, aggregatedStatistics.getCount()); + + // There is a map for every sport. + Assert.assertTrue(aggregatedStatistics.get(biking) != null); + Assert.assertTrue(aggregatedStatistics.get(running) != null); + Assert.assertTrue(aggregatedStatistics.get(walking) != null); + Assert.assertTrue(aggregatedStatistics.get(driving) != null); + + // Number of tracks by sport. + Assert.assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking. + Assert.assertEquals(2, aggregatedStatistics.get(running).getCountTracks()); // Running. + Assert.assertEquals(2, aggregatedStatistics.get(walking).getCountTracks()); // Walking. + Assert.assertEquals(1, aggregatedStatistics.get(driving).getCountTracks()); // Driving. + + // Biking. + { + TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); + Assert.assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime * 5, statistics2.getMovingTime()); + } + + // Running. + { + TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics(); + Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime * 2, statistics2.getMovingTime()); + } + + // Walking. + { + TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics(); + Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime * 2, statistics2.getMovingTime()); + } + + // Driving. + { + TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics(); + Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0); + Assert.assertEquals(totalTime, statistics2.getMovingTime()); + } + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 15d669c6a..12e330435 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -98,6 +98,11 @@ limitations under the License. android:value=".SearchListActivity" /> + + diff --git a/src/main/java/de/dennisguse/opentracks/AggregatedStatisticsActivity.java b/src/main/java/de/dennisguse/opentracks/AggregatedStatisticsActivity.java new file mode 100644 index 000000000..292b2ae6f --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/AggregatedStatisticsActivity.java @@ -0,0 +1,41 @@ +package de.dennisguse.opentracks; + +import android.os.Bundle; +import android.widget.ListView; + +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter; +import de.dennisguse.opentracks.viewmodels.AggregatedStatistics; +import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel; + +public class AggregatedStatisticsActivity extends AbstractActivity { + + private ListView listView; + private AggregatedStatisticsAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + listView = findViewById(R.id.aggregated_stats_list); + + final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class); + viewModel.getAggregatedStats().observe(this, new Observer() { + @Override + public void onChanged(AggregatedStatistics aggregatedStatistics) { + if (aggregatedStatistics != null) { + adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics); + listView.setAdapter(adapter); + } + adapter.notifyDataSetChanged(); + } + }); + } + + @Override + protected int getLayoutResId() { + return R.layout.aggregated_stats; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index acdc786f7..636e7a0da 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -396,6 +396,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe this.invalidateOptionsMenu(); } return true; + case R.id.track_list_aggregated_stats: + intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class); + startActivity(intent); + return true; case R.id.track_list_markers: intent = IntentUtils.newIntent(this, MarkerListActivity.class); startActivity(intent); diff --git a/src/main/java/de/dennisguse/opentracks/adapters/AggregatedStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/AggregatedStatisticsAdapter.java new file mode 100644 index 000000000..a400c1141 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/adapters/AggregatedStatisticsAdapter.java @@ -0,0 +1,181 @@ +package de.dennisguse.opentracks.adapters; + +import android.content.Context; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.util.StringUtils; +import de.dennisguse.opentracks.util.TrackIconUtils; +import de.dennisguse.opentracks.viewmodels.AggregatedStatistics; + +public class AggregatedStatisticsAdapter extends BaseAdapter { + + private AggregatedStatistics aggregatedStatistics; + private Context context; + + public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) { + this.context = context; + this.aggregatedStatistics = aggregatedStatistics; + } + + @Override + public int getCount() { + if (aggregatedStatistics != null) { + return aggregatedStatistics.getCount(); + } + return 0; + } + + @Override + public AggregatedStatistics.AggregatedStatistic getItem(int position) { + return aggregatedStatistics.getItem(position); + } + + @Override + public long getItemId(int position) { + // Not important because data are not database register but cooked data. + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + AggregatedStatistics.AggregatedStatistic aggregatedStats = getItem(position); + ViewHolder viewHolder; + + if (isSpeedSport(position)) { + viewHolder = new ViewSpeedHolder(); + } else { + viewHolder = new ViewPaceHolder(); + } + + convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false); + viewHolder.setValues(convertView, getIcon(position), getName(position), aggregatedStats); + + return convertView; + } + + private boolean isSpeedSport(int position) { + String category = aggregatedStatistics.getSportName(position); + return TrackIconUtils.isSpeedIcon(context, category); + } + + private int getIcon(int position) { + String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistics.getSportName(position)); + return TrackIconUtils.getIconDrawable(iconValue); + } + + private String getName(int position) { + return aggregatedStatistics.getSportName(position); + } + + private class ViewHolder { + protected ImageView sportIcon; + protected TextView typeLabel; + protected TextView numTracks; + protected TextView distance; + protected TextView distanceUnit; + protected TextView time; + protected boolean metricsUnits; + + public ViewHolder() { + metricsUnits = PreferencesUtils.isMetricUnits(context); + } + + public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) { + sportIcon = view.findViewById(R.id.aggregated_stats_sport_icon); + typeLabel = view.findViewById(R.id.aggregated_stats_type_label); + numTracks = view.findViewById(R.id.aggregated_stats_num_tracks); + distance = view.findViewById(R.id.aggregated_stats_distance); + distanceUnit = view.findViewById(R.id.aggregated_stats_distance_unit); + time = view.findViewById(R.id.aggregated_stats_time); + + sportIcon.setImageResource(iconDrawable); + typeLabel.setText(name); + numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStats.getCountTracks()))); + + Pair parts = StringUtils.getDistanceParts(context, aggregatedStats.getTrackStatistics().getTotalDistance(), metricsUnits); + distance.setText(parts.first); + distanceUnit.setText(parts.second); + + time.setText(StringUtils.formatElapsedTime(aggregatedStats.getTrackStatistics().getMovingTime())); + } + } + + private class ViewSpeedHolder extends ViewHolder { + private TextView avgSpeed; + private TextView avgSpeedUnit; + private TextView avgSpeedLabel; + private TextView maxSpeed; + private TextView maxSpeedUnit; + private TextView maxSpeedLabel; + + @Override + public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) { + super.setValues(view, iconDrawable, name, aggregatedStats); + + avgSpeed = view.findViewById(R.id.aggregated_stats_avg_rate); + avgSpeedUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit); + avgSpeedLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label); + maxSpeed = view.findViewById(R.id.aggregated_stats_max_rate); + maxSpeedUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit); + maxSpeedLabel = view.findViewById(R.id.aggregated_stats_max_rate_label); + + { + Pair parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, true); + avgSpeed.setText(parts.first); + avgSpeedUnit.setText(parts.second); + avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed)); + } + + { + Pair parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, true); + maxSpeed.setText(parts.first); + maxSpeedUnit.setText(parts.second); + maxSpeedLabel.setText(context.getString(R.string.stats_max_speed)); + } + } + } + + private class ViewPaceHolder extends ViewHolder { + private TextView avgPace; + private TextView avgPaceUnit; + private TextView avgPaceLabel; + private TextView maxPace; + private TextView maxPaceUnit; + private TextView maxPaceLabel; + + @Override + public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) { + super.setValues(view, iconDrawable, name, aggregatedStats); + + avgPace = view.findViewById(R.id.aggregated_stats_avg_rate); + avgPaceUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit); + avgPaceLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label); + + maxPace = view.findViewById(R.id.aggregated_stats_max_rate); + maxPaceUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit); + maxPaceLabel = view.findViewById(R.id.aggregated_stats_max_rate_label); + + { + Pair parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, false); + avgPace.setText(parts.first); + avgPaceUnit.setText(parts.second); + avgPaceLabel.setText(R.string.stats_average_moving_pace); + } + + { + Pair parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, false); + maxPace.setText(parts.first); + maxPaceUnit.setText(parts.second); + maxPaceLabel.setText(R.string.stats_fastest_pace); + } + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java index 335e8a045..3d9c5d40f 100644 --- a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java @@ -426,4 +426,8 @@ public class StringUtils { } return new Pair<>(value, unit); } + + public static String valueInParentheses(String text) { + return "(" + text + ")"; + } } diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatistics.java b/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatistics.java new file mode 100644 index 000000000..bae885266 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatistics.java @@ -0,0 +1,74 @@ +package de.dennisguse.opentracks.viewmodels; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import de.dennisguse.opentracks.content.data.Track; +import de.dennisguse.opentracks.stats.TrackStatistics; +import de.dennisguse.opentracks.util.TrackIconUtils; + +public class AggregatedStatistics { + + private HashMap aggregatedStatistics = new HashMap<>(); + + public void aggregate(List tracks) { + for (Track track : tracks) { + aggregate(track); + } + } + + public void aggregate(Track track) { + String category = track.getCategory(); + if (aggregatedStatistics.containsKey(category)) { + aggregatedStatistics.get(category).add(track.getTrackStatistics()); + } else { + aggregatedStatistics.put(category, new AggregatedStatistic(track.getTrackStatistics())); + } + } + + public int getCount() { + return aggregatedStatistics.size(); + } + + public String getSportName(int position) { + return getKey(position); + } + + public AggregatedStatistic get(String category) { + return aggregatedStatistics.get(category); + } + + private String getKey(int position) { + return (new ArrayList<>(aggregatedStatistics.keySet())).get(position); + } + + public AggregatedStatistic getItem(int position) { + return (new ArrayList<>(aggregatedStatistics.values())).get(position); + } + + public static class AggregatedStatistic { + private TrackStatistics trackStatistics; + private int countTracks; + + public AggregatedStatistic(TrackStatistics trackStatistics) { + this.trackStatistics = trackStatistics; + this.countTracks = 1; + } + + public TrackStatistics getTrackStatistics() { + return trackStatistics; + } + + public int getCountTracks() { + return countTracks; + } + + void add(TrackStatistics statistics) { + trackStatistics.merge(statistics); + countTracks++; + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsModel.java new file mode 100644 index 000000000..7d97414a1 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsModel.java @@ -0,0 +1,42 @@ +package de.dennisguse.opentracks.viewmodels; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.util.List; + +import de.dennisguse.opentracks.content.data.Track; +import de.dennisguse.opentracks.content.provider.ContentProviderUtils; + +public class AggregatedStatisticsModel extends AndroidViewModel { + + private MutableLiveData aggregatedStats; + + public AggregatedStatisticsModel(@NonNull Application application) { + super(application); + } + + public LiveData getAggregatedStats() { + if (aggregatedStats == null) { + aggregatedStats = new MutableLiveData<>(); + loadAggregatedStats(); + } + return aggregatedStats; + } + + private void loadAggregatedStats() { + new Thread(() -> { + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext()); + List tracks = contentProviderUtils.getAllTracks(); + + AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(); + aggregatedStatistics.aggregate(tracks); + + aggregatedStats.postValue(aggregatedStatistics); + }).start(); + } +} diff --git a/src/main/res/drawable/ic_statistics_24dp.xml b/src/main/res/drawable/ic_statistics_24dp.xml new file mode 100644 index 000000000..13cfc8fad --- /dev/null +++ b/src/main/res/drawable/ic_statistics_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/res/layout/aggregated_stats.xml b/src/main/res/layout/aggregated_stats.xml new file mode 100644 index 000000000..817cb5357 --- /dev/null +++ b/src/main/res/layout/aggregated_stats.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/aggregated_stats_list_item.xml b/src/main/res/layout/aggregated_stats_list_item.xml new file mode 100644 index 000000000..32d7e7e5e --- /dev/null +++ b/src/main/res/layout/aggregated_stats_list_item.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/track_list.xml b/src/main/res/menu/track_list.xml index 8cc4290d0..9667c57fd 100644 --- a/src/main/res/menu/track_list.xml +++ b/src/main/res/menu/track_list.xml @@ -28,6 +28,11 @@ limitations under the License. android:title="@string/menu_start_gps" android:visible="false" app:showAsAction="ifRoom" /> + true @android:color/white + +