From 838671e2cedd7c19ca98b3b2e442b38bb5e3bc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 3 Jun 2021 17:16:03 +0200 Subject: [PATCH 1/4] IntervalsFragment doesn't use TrackDataHub anymore. This improve intervals performance. Fixes #808. --- .../opentracks/EspressoDeleteTrackTest.java | 1 - .../util/AnnouncementUtilsTest.java | 12 +- .../viewmodels/IntervalStatisticsTest.java | 3 +- .../opentracks/TrackRecordedActivity.java | 2 +- .../opentracks/TrackRecordingActivity.java | 2 +- .../adapters/IntervalStatisticsAdapter.java | 4 - .../provider/ContentProviderUtils.java | 10 ++ .../fragments/IntervalsFragment.java | 119 ++++------------ .../tasks/AnnouncementPeriodicTask.java | 3 +- .../viewmodels/IntervalStatistics.java | 45 ++++-- .../viewmodels/IntervalStatisticsModel.java | 128 ++++++++++++------ 11 files changed, 166 insertions(+), 163 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java index 4efb59a93..a3456fb2c 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoDeleteTrackTest.java @@ -4,7 +4,6 @@ package de.dennisguse.opentracks; import android.app.ActivityManager; import android.app.Instrumentation; import android.content.Context; -import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java index 003f2d86f..d9b3ff178 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java @@ -52,7 +52,8 @@ public class AnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000), Distance.of(0)); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + intervalStatistics.addTrackPoints(trackPoints); IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); // when @@ -88,7 +89,8 @@ public class AnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000)); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + intervalStatistics.addTrackPoints(trackPoints); IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); // when @@ -124,7 +126,8 @@ public class AnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000)); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + intervalStatistics.addTrackPoints(trackPoints); IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); // when @@ -160,7 +163,8 @@ public class AnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000)); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + intervalStatistics.addTrackPoints(trackPoints); IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); // when diff --git a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java index a0d558a1e..c90b96a1c 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java @@ -114,7 +114,8 @@ public class IntervalStatisticsTest { } private void whenAndThen(List trackPoints, TrackStatistics trackStatistics, float distanceInterval) { - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(distanceInterval), Distance.of(0)); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0)); + intervalStatistics.addTrackPoints(trackPoints); List intervalList = intervalStatistics.getIntervalList(); Distance totalDistance = Distance.of(0); float totalTime = 0L; diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java index 599e74481..f832e46d4 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java @@ -289,7 +289,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement case 0: return StatisticsRecordedFragment.newInstance(trackId); case 1: - return IntervalsFragment.newInstance(true); + return IntervalsFragment.newInstance(trackId, true); case 2: return ChartFragment.newInstance(false); case 3: diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java index f9054622d..29faca1a8 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java @@ -360,7 +360,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc case 0: return StatisticsRecordingFragment.newInstance(); case 1: - return IntervalsFragment.newInstance(false); + return IntervalsFragment.newInstance(trackId, false); case 2: return ChartFragment.newInstance(false); case 3: diff --git a/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java index 9b528ee2d..fed2e69e2 100644 --- a/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/adapters/IntervalStatisticsAdapter.java @@ -72,10 +72,6 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter swapData(List data, boolean metricUnits, boolean isReportSpeed) { - if (intervalList == data && this.metricUnits == metricUnits && this.isReportSpeed == isReportSpeed) { - return null; - } - this.metricUnits = metricUnits; this.isReportSpeed = isReportSpeed; intervalList = data; diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 0b6e85578..693a2fa70 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -745,6 +745,16 @@ public class ContentProviderUtils { return new TrackPointIterator(this, trackId, startTrackPointId); } + public List getTrackPoints(final Track.Id trackId, final TrackPoint.Id startTrackPointId) { + List trackPoints = new ArrayList<>(); + try (TrackPointIterator trackPointIterator = getTrackPointLocationIterator(trackId, startTrackPointId)) { + while (trackPointIterator.hasNext()) { + trackPoints.add(trackPointIterator.next()); + } + } + return trackPoints; + } + @Deprecated private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) { try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) { diff --git a/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java index 8830cbdae..f9cefd294 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java @@ -12,7 +12,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; @@ -20,15 +19,10 @@ import androidx.recyclerview.widget.LinearLayoutManager; import java.util.List; import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.TrackActivityDataHubInterface; import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter; -import de.dennisguse.opentracks.content.TrackDataHub; -import de.dennisguse.opentracks.content.TrackDataListener; -import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.Track; -import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.databinding.IntervalListViewBinding; -import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; @@ -36,31 +30,31 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; /** * A fragment to display the intervals from recorded track. */ -public class IntervalsFragment extends Fragment implements TrackDataListener { +public class IntervalsFragment extends Fragment { private static final String TAG = IntervalsFragment.class.getSimpleName(); private static final String FROM_TOP_TO_BOTTOM_KEY = "fromTopToBottom"; + private static final String TRACK_ID_KEY = "trackId"; private IntervalStatisticsModel viewModel; protected IntervalStatisticsAdapter.StackMode stackModeListView; private IntervalStatisticsModel.IntervalOption selectedInterval; + private Track.Id trackId; private boolean metricUnits; private IntervalStatisticsAdapter adapter; private ArrayAdapter spinnerAdapter; private SharedPreferences sharedPreferences; - private TrackDataHub trackDataHub; private boolean isReportSpeed; private IntervalListViewBinding viewBinding; protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { - metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext()); - uploadIntervals(); + updateIntervals(PreferencesUtils.isMetricUnits(sharedPreferences, getContext()), selectedInterval); if (spinnerAdapter != null) { spinnerAdapter.notifyDataSetChanged(); } @@ -70,12 +64,14 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { /** * Creates an instance of this class. * + * @param trackId track's id. * @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top. * @return IntervalsFragment instance. */ - public static Fragment newInstance(boolean fromTopToBottom) { + public static Fragment newInstance(Track.Id trackId, boolean fromTopToBottom) { Bundle bundle = new Bundle(); bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom); + bundle.putParcelable(TRACK_ID_KEY, trackId); IntervalsFragment intervalsFragment = new IntervalsFragment(); intervalsFragment.setArguments(bundle); return intervalsFragment; @@ -85,6 +81,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); stackModeListView = getArguments().getBoolean(FROM_TOP_TO_BOTTOM_KEY, true) ? IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP : IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM; + trackId = getArguments().getParcelable(TRACK_ID_KEY); } @Override @@ -127,8 +124,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { viewBinding.spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i]; - uploadIntervals(); + updateIntervals(metricUnits, IntervalStatisticsModel.IntervalOption.values()[i]); } @Override @@ -141,11 +137,15 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { public void onResume() { super.onResume(); - resumeTrackDataHub(); - sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getContext()); + Track track = contentProviderUtils.getTrack(trackId); + if (track != null) { + isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), track.getCategory()); + } + viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class); loadIntervals(); } @@ -153,8 +153,8 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { @Override public void onPause() { super.onPause(); - pauseTrackDataHub(); sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); + viewModel.onPause(); } @Override @@ -180,88 +180,17 @@ public class IntervalsFragment extends Fragment implements TrackDataListener { if (viewModel == null) { return; } - LiveData> liveData = viewModel.getIntervalStats(metricUnits, selectedInterval); + LiveData> liveData = viewModel.getIntervalStats(trackId, metricUnits, selectedInterval); liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed)); } - private synchronized void uploadIntervals() { - if (viewModel == null) { - return; - } - viewModel.upload(metricUnits, selectedInterval); - } + private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) { + boolean update = metricUnits != this.metricUnits || !selectedInterval.equals(this.selectedInterval); + this.metricUnits = metricUnits; + this.selectedInterval = selectedInterval; - /** - * Resumes the trackDataHub. - * Needs to be synchronized because trackDataHub can be accessed by multiple threads. - */ - private synchronized void resumeTrackDataHub() { - trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub(); - trackDataHub.registerTrackDataListener(this); - } - - /** - * Pauses the trackDataHub. - * Needs to be synchronized because trackDataHub can be accessed by multiple threads. - */ - private synchronized void pauseTrackDataHub() { - trackDataHub.unregisterTrackDataListener(this); - trackDataHub = null; - } - - @Override - public void onTrackUpdated(Track track) { - if (isResumed()) { - getActivity().runOnUiThread(() -> { - if (isResumed()) { - // Set category. - String category = track != null ? track.getCategory() : ""; - - // Set rate label. - isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category); - viewBinding.intervalRate.setText(isReportSpeed ? R.string.stats_speed : R.string.stats_pace); - } - }); - } - } - - @Override - public void clearTrackPoints() { - if (isResumed() && viewModel != null) { - viewModel.clear(); - } - } - - @Override - public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused, Speed unused2, double unused3) { - if (isResumed() && viewModel != null) { - viewModel.add(trackPoint); - } - } - - @Override - public void onSampledOutTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics unused) { - if (isResumed() && viewModel != null) { - viewModel.add(trackPoint); - } - } - - @Override - public void onNewTrackPointsDone() { - if (isResumed() && viewModel != null) { - runOnUiThread(viewModel::onNewTrackPoints); - } - } - - /** - * Runs a runnable on the UI thread. - * - * @param runnable the runnable - */ - private void runOnUiThread(Runnable runnable) { - FragmentActivity fragmentActivity = getActivity(); - if (fragmentActivity != null) { - fragmentActivity.runOnUiThread(runnable); + if (update && viewModel != null) { + viewModel.update(trackId, this.metricUnits, this.selectedInterval); } } } diff --git a/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java b/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java index 722a5974c..315653077 100644 --- a/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java +++ b/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java @@ -189,7 +189,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask { boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category); Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.one(isMetricUnits), minGPSDistance); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance); + intervalStatistics.addTrackPoints(trackPoints); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval); diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java index 4650a36c2..d3ff8716e 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java @@ -14,25 +14,32 @@ import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; public class IntervalStatistics { - private final List intervalList = new ArrayList<>(); + private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(); + private final List intervalList; private final Distance distanceInterval; + private final Distance minGPSDistance; + private Interval interval, lastInterval; /** - * @param trackPoints the list of TrackPoint. * @param distanceInterval distance of every interval. * @param minGPSDistance the setting value for GPS distance. */ - public IntervalStatistics(@NonNull List trackPoints, Distance distanceInterval, Distance minGPSDistance) { - intervalList.clear(); + public IntervalStatistics(Distance distanceInterval, Distance minGPSDistance) { this.distanceInterval = distanceInterval; + this.minGPSDistance = minGPSDistance; + interval = new Interval(); + lastInterval = new Interval(); + intervalList = new ArrayList<>(); + intervalList.add(lastInterval); + } + + public void addTrackPoints(@NonNull List trackPoints) { if (trackPoints.size() == 0) { return; } - TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(); - Interval interval = new Interval(); - + boolean newIntervalAdded = false; for (TrackPoint trackPoint : trackPoints) { trackStatisticsUpdater.addTrackPoint(trackPoint, minGPSDistance); @@ -43,17 +50,24 @@ public class IntervalStatistics { Interval adjustedInterval = new Interval(interval); adjustedInterval.adjust(adjustFactor); + intervalList.remove(intervalList.size() - 1); intervalList.add(adjustedInterval); interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time)); trackStatisticsUpdater = new TrackStatisticsUpdater(); trackStatisticsUpdater.addTrackPoint(trackPoint, minGPSDistance); + + lastInterval = new Interval(interval); + intervalList.add(lastInterval); + + newIntervalAdded = true; } } - if (trackStatisticsUpdater.getTrackStatistics().getTotalDistance().toM() >= 1d) { - interval.update(trackStatisticsUpdater.getTrackStatistics(), null); - intervalList.add(interval); + if (newIntervalAdded) { + lastInterval.update(trackStatisticsUpdater.getTrackStatistics(), null); + } else { + lastInterval.set(trackStatisticsUpdater.getTrackStatistics()); } } @@ -110,7 +124,7 @@ public class IntervalStatistics { loss_m = i.loss_m; } - public void adjust(double adjustFactor) { + private void adjust(double adjustFactor) { distance = distance.multipliedBy(adjustFactor); time = Duration.ofMillis((long) (time.toMillis() * adjustFactor)); } @@ -131,7 +145,7 @@ public class IntervalStatistics { return loss_m; } - public void update(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) { + private void update(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) { distance = distance.plus(trackStatistics.getTotalDistance()); time = time.plus(trackStatistics.getTotalTime()); gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; @@ -141,5 +155,12 @@ public class IntervalStatistics { loss_m = lastTrackPoint.hasAltitudeLoss() ? loss_m - lastTrackPoint.getAltitudeLoss() : loss_m; } } + + private void set(TrackStatistics trackStatistics) { + distance = trackStatistics.getTotalDistance(); + time = trackStatistics.getTotalTime(); + gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; + loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m; + } } } diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java index ce2d1f944..0c8e53f1f 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java @@ -1,19 +1,27 @@ package de.dennisguse.opentracks.viewmodels; import android.app.Application; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MutableLiveData; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import de.dennisguse.opentracks.content.data.Distance; +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.provider.ContentProviderUtils; import de.dennisguse.opentracks.util.PreferencesUtils; -import de.dennisguse.opentracks.util.UnitConversions; /** * This model is used to load intervals for a track. @@ -21,65 +29,95 @@ import de.dennisguse.opentracks.util.UnitConversions; */ public class IntervalStatisticsModel extends AndroidViewModel { - private final List trackPoints = new ArrayList<>(); + private static final String TAG = IntervalStatisticsModel.class.getSimpleName(); + private MutableLiveData> intervalsLiveData; + private IntervalStatistics intervalStatistics; private Distance distanceInterval; - private Distance minGPSDistance; + private final Distance minGPSDistance; + private final ContentResolver contentResolver; + private ContentObserver trackPointsTableObserver; + private TrackPoint.Id lastTrackPointId; + + private final Executor executor = Executors.newSingleThreadExecutor(); + private HandlerThread handlerThread; + private Handler handler; public IntervalStatisticsModel(@NonNull Application application) { super(application); minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(PreferencesUtils.getSharedPreferences(application), application); + contentResolver = getApplication().getContentResolver(); } - public MutableLiveData> getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) { - synchronized (trackPoints) { - if (intervalsLiveData == null) { - if (interval == null) { - interval = IntervalOption.OPTION_1; - } - - intervalsLiveData = new MutableLiveData<>(); - distanceInterval = interval.getDistance(metricUnits); - loadIntervalStatistics(); - } - return intervalsLiveData; + @Override + protected void onCleared() { + super.onCleared(); + if (trackPointsTableObserver != null) { + contentResolver.unregisterContentObserver(trackPointsTableObserver); } - } - - private void loadIntervalStatistics() { - IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval, minGPSDistance); - intervalsLiveData.postValue(intervalStatistics.getIntervalList()); - } - - public void add(TrackPoint trackPoint) { - synchronized (trackPoints) { - trackPoints.add(trackPoint); + if (handlerThread != null) { + handlerThread.getLooper().quit(); + handlerThread = null; } + handler = null; } - public void onNewTrackPoints() { - synchronized (trackPoints) { - if (intervalsLiveData != null) { - loadIntervalStatistics(); - } - } - } - - public void clear() { - synchronized (trackPoints) { - trackPoints.clear(); - } - } - - public void upload(boolean metricUnits, @Nullable IntervalOption interval) { - synchronized (trackPoints) { + public MutableLiveData> getIntervalStats(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) { + if (intervalsLiveData == null) { if (interval == null) { interval = IntervalOption.OPTION_1; } + intervalsLiveData = new MutableLiveData<>(); distanceInterval = interval.getDistance(metricUnits); - loadIntervalStatistics(); + intervalStatistics = new IntervalStatistics(distanceInterval, minGPSDistance); + + loadIntervalStatistics(trackId); } + + registerTrackPointsObserver(trackId); + + return intervalsLiveData; + } + + private void loadIntervalStatistics(Track.Id trackId) { + executor.execute(() -> { + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication()); + List trackPoints = contentProviderUtils.getTrackPoints(trackId, lastTrackPointId); + lastTrackPointId = trackPoints.size() > 0 ? trackPoints.get(trackPoints.size() - 1).getId() : lastTrackPointId; + intervalStatistics.addTrackPoints(trackPoints); + intervalsLiveData.postValue(intervalStatistics.getIntervalList()); + }); + } + + private void registerTrackPointsObserver(Track.Id trackId) { + handlerThread = new HandlerThread(TAG); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + trackPointsTableObserver = new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + loadIntervalStatistics(trackId); + } + }; + contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_TRACKID, false, trackPointsTableObserver); + } + + public void onPause() { + if (trackPointsTableObserver != null) { + contentResolver.unregisterContentObserver(trackPointsTableObserver); + } + } + + public void update(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) { + if (interval == null) { + interval = IntervalOption.OPTION_1; + } + + lastTrackPointId = null; + distanceInterval = interval.getDistance(metricUnits); + intervalStatistics = new IntervalStatistics(distanceInterval, minGPSDistance); + loadIntervalStatistics(trackId); } /** @@ -107,6 +145,10 @@ public class IntervalStatisticsModel extends AndroidViewModel { .multipliedBy(multiplier); } + public boolean equals(IntervalOption intervalOption) { + return intervalOption != null && this.multiplier == intervalOption.multiplier; + } + @Override public String toString() { return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer. From b2c2047ca2f90006a3b84fbf7782b8786fcd1304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 10 Jun 2021 15:52:37 +0200 Subject: [PATCH 2/4] Dennis Review. --- .../opentracks/content/data/TestDataUtil.java | 17 ++ .../util/AnnouncementUtilsTest.java | 170 ++++++++++++------ .../viewmodels/IntervalStatisticsTest.java | 48 ++--- .../provider/ContentProviderUtils.java | 11 +- .../fragments/IntervalsFragment.java | 4 +- .../tasks/AnnouncementPeriodicTask.java | 10 +- .../viewmodels/IntervalStatistics.java | 30 ++-- .../viewmodels/IntervalStatisticsModel.java | 38 ++-- 8 files changed, 206 insertions(+), 122 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java index 80cee95cf..128a80979 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java @@ -14,6 +14,7 @@ import java.util.List; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.stats.TrackStatistics; +import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.FileUtils; public class TestDataUtil { @@ -166,4 +167,20 @@ public class TestDataUtil { } return trackPoints; } + + public static Pair buildTrackWithTrackPoints(ContentProviderUtils contentProviderUtils, int numberOfPoints) { + Track dummyTrack = new Track(); + dummyTrack.setId(new Track.Id(System.currentTimeMillis())); + dummyTrack.setName("Dummy Track"); + contentProviderUtils.insertTrack(dummyTrack); + TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(); + for (int i = 0; i < numberOfPoints; i++) { + TrackPoint tp = TestDataUtil.createTrackPoint(i); + contentProviderUtils.insertTrackPoint(tp, dummyTrack.getId()); + trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0)); + } + dummyTrack.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics()); + contentProviderUtils.updateTrack(dummyTrack); + return new Pair<>(dummyTrack.getId(), trackStatisticsUpdater.getTrackStatistics()); + } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java index d9b3ff178..670980f24 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/AnnouncementUtilsTest.java @@ -1,21 +1,23 @@ package de.dennisguse.opentracks.util; import android.content.Context; +import android.util.Pair; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.time.Duration; -import java.util.List; import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Speed; 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.provider.ContentProviderUtils; +import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics; @@ -25,6 +27,12 @@ import static org.junit.Assert.assertEquals; public class AnnouncementUtilsTest { private final Context context = ApplicationProvider.getApplicationContext(); + private ContentProviderUtils contentProviderUtils; + + @Before + public void setUp() { + contentProviderUtils = new ContentProviderUtils(context); + } @Test public void getAnnouncement_metric_speed() { @@ -44,23 +52,31 @@ public class AnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_metric_speed() { - TrackStatistics stats = new TrackStatistics(); - stats.setTotalDistance(Distance.of(20000)); - stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); - stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); - stats.setMaxSpeed(Speed.of(100)); - stats.setTotalAltitudeGain(6000f); - - List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); - intervalStatistics.addTrackPoints(trackPoints); - IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + // given + int numberOfPoints = 1000; + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics stats = trackWithStats.second; + IntervalStatistics.Interval lastInterval; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + } // when String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval); // then - assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour Lap speed of 51.2 kilometers per hour", announcement); + assertEquals( + "OpenTracks total distance " + + StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first + + " kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + + StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), true, true).first + + " kilometers per hour Lap speed of " + + StringUtils.getSpeedParts(context, lastInterval.getSpeed(), true, true).first + + " kilometers per hour", + announcement); } @Test @@ -81,23 +97,31 @@ public class AnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_metric_pace() { - TrackStatistics stats = new TrackStatistics(); - stats.setTotalDistance(Distance.of(20000)); - stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); - stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); - stats.setMaxSpeed(Speed.of(100)); - stats.setTotalAltitudeGain(6000f); - - List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); - intervalStatistics.addTrackPoints(trackPoints); - IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + // given + int numberOfPoints = 1000; + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics stats = trackWithStats.second; + IntervalStatistics.Interval lastInterval; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + } // when String announcement = AnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval); // then - assertEquals("OpenTracks total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer Lap time of 1 minute 10 seconds per kilometer", announcement); + assertEquals( + "OpenTracks total distance " + + StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first + + " kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + + buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(true), true) + + " per kilometer Lap time of " + + buildAndGetTimeText(lastInterval.getSpeed().toPace(true), true) + + " per kilometer", + announcement); } @Test @@ -118,23 +142,31 @@ public class AnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_imperial_speed() { - TrackStatistics stats = new TrackStatistics(); - stats.setTotalDistance(Distance.of(20000)); - stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); - stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); - stats.setMaxSpeed(Speed.of(100)); - stats.setTotalAltitudeGain(6000f); - - List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); - intervalStatistics.addTrackPoints(trackPoints); - IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + // given + int numberOfPoints = 1000; + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics stats = trackWithStats.second; + IntervalStatistics.Interval lastInterval; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + } // when String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval); // then - assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour Lap speed of 31.8 miles per hour", announcement); + assertEquals( + "OpenTracks total distance " + + StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first + + " miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + + StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), false, true).first + + " miles per hour Lap speed of " + + StringUtils.getSpeedParts(context, lastInterval.getSpeed(), false, true).first + + " miles per hour", + announcement); } @Test @@ -155,22 +187,60 @@ public class AnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_imperial_pace() { - TrackStatistics stats = new TrackStatistics(); - stats.setTotalDistance(Distance.of(20000)); - stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); - stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); - stats.setMaxSpeed(Speed.of(100)); - stats.setTotalAltitudeGain(6000f); - - List trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; - IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); - intervalStatistics.addTrackPoints(trackPoints); - IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + // given + int numberOfPoints = 1000; + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics stats = trackWithStats.second; + IntervalStatistics.Interval lastInterval; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000), Distance.of(0)); + lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + } // when String announcement = AnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval); // then - assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement); + //assertEquals("OpenTracks total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement); + assertEquals( + "OpenTracks total distance " + + StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first + + " miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + + buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(false), true) + + " per mile Lap time of " + + buildAndGetTimeText(lastInterval.getSpeed().toPace(false), true) + + " per mile", + announcement); + } + + /** + * Builds an returns the text representing the duration's time. + * + * @param duration Duration object. + * @param showFromMinutes show minutes tough it's 0. + * @return text representing the duratin's time. + */ + private String buildAndGetTimeText(Duration duration, boolean showFromMinutes) { + long hours = Math.abs(duration.getSeconds()) / 3600; + long minutes = (Math.abs(duration.getSeconds()) % 3600) / 60; + long seconds = Math.abs(duration.getSeconds()) % 60; + String hUnit = hours > 1 || hours == 0 ? "hours" : "hour"; + String mUnit = minutes > 1 || minutes == 0 ? "minutes" : "minute"; + String sUnit = seconds > 1 || seconds == 0 ? "seconds" : "second"; + + String res = hours > 0 ? hours + " " + hUnit : ""; + if (hours > 0) { + res += minutes > 0 || showFromMinutes ? " " + minutes + " " + mUnit : ""; + } else { + res += minutes > 0 || showFromMinutes ? minutes + " " + mUnit : ""; + } + if (hours > 0 || minutes > 0 || showFromMinutes) { + res += " " + seconds + " " + sUnit; + } else { + res += seconds + " " + sUnit; + } + return res; } } \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java index c90b96a1c..f1a6a9424 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsTest.java @@ -1,5 +1,11 @@ package de.dennisguse.opentracks.viewmodels; +import android.content.Context; +import android.util.Pair; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -10,6 +16,8 @@ import de.dennisguse.opentracks.content.data.Distance; 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.provider.ContentProviderUtils; +import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.UnitConversions; @@ -21,8 +29,12 @@ public class IntervalStatisticsTest { private static final String TAG = IntervalStatisticsTest.class.getSimpleName(); - private List buildTrackPoints(int numberOfTrackPoints) { - return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second; + private final Context context = ApplicationProvider.getApplicationContext(); + private ContentProviderUtils contentProviderUtils; + + @Before + public void setUp() { + contentProviderUtils = new ContentProviderUtils(context); } private TrackStatistics buildTrackStatistics(List trackPoints) { @@ -41,12 +53,10 @@ public class IntervalStatisticsTest { // With 50 points and interval distance of 1000m. // given - List trackPoints = buildTrackPoints(50); - TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); float distanceInterval = 1000f; // when and then - whenAndThen(trackPoints, trackStatistics, distanceInterval); + whenAndThen(50, distanceInterval); } /** @@ -57,12 +67,10 @@ public class IntervalStatisticsTest { // With 200 points and interval distance of 1000m. // given - List trackPoints = buildTrackPoints(200); - TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); float distanceInterval = 1000f; // when and then - whenAndThen(trackPoints, trackStatistics, distanceInterval); + whenAndThen(200, distanceInterval); } /** @@ -73,12 +81,10 @@ public class IntervalStatisticsTest { // With 200 points and interval distance of 3000m. // given - List trackPoints = buildTrackPoints(200); - TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); float distanceInterval = 3000f; // when and then - whenAndThen(trackPoints, trackStatistics, distanceInterval); + whenAndThen(3000, distanceInterval); } /** @@ -89,12 +95,10 @@ public class IntervalStatisticsTest { // With 1000 points and interval distance of 3000m. // given - List trackPoints = buildTrackPoints(1000); - TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); float distanceInterval = 3000f; // when and then - whenAndThen(trackPoints, trackStatistics, distanceInterval); + whenAndThen(1000, distanceInterval); } /** @@ -105,17 +109,21 @@ public class IntervalStatisticsTest { // With 10000 points and interval distance of 1000m. // given - List trackPoints = buildTrackPoints(10000); - TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); float distanceInterval = 1000f; // when and then - whenAndThen(trackPoints, trackStatistics, distanceInterval); + whenAndThen(10000, distanceInterval); } - private void whenAndThen(List trackPoints, TrackStatistics trackStatistics, float distanceInterval) { + private void whenAndThen(int numberOfPoints, float distanceInterval) { IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(distanceInterval), Distance.of(0)); - intervalStatistics.addTrackPoints(trackPoints); + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics trackStatistics = trackWithStats.second; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + intervalStatistics.addTrackPoints(trackPointIterator); + } List intervalList = intervalStatistics.getIntervalList(); Distance totalDistance = Distance.of(0); float totalTime = 0L; @@ -130,7 +138,7 @@ public class IntervalStatisticsTest { assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01); assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1); assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval)); - assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1); + assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1); for (int i = 0; i < intervalList.size() - 1; i++) { assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001); diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 693a2fa70..605dbecd3 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -37,6 +37,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.UUID; +import java.util.stream.StreamSupport; import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.content.data.Altitude; @@ -745,16 +746,6 @@ public class ContentProviderUtils { return new TrackPointIterator(this, trackId, startTrackPointId); } - public List getTrackPoints(final Track.Id trackId, final TrackPoint.Id startTrackPointId) { - List trackPoints = new ArrayList<>(); - try (TrackPointIterator trackPointIterator = getTrackPointLocationIterator(trackId, startTrackPointId)) { - while (trackPointIterator.hasNext()) { - trackPoints.add(trackPointIterator.next()); - } - } - return trackPoints; - } - @Deprecated private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) { try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) { diff --git a/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java index f9cefd294..27db82638 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsFragment.java @@ -64,7 +64,7 @@ public class IntervalsFragment extends Fragment { /** * Creates an instance of this class. * - * @param trackId track's id. + * @param trackId * @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top. * @return IntervalsFragment instance. */ @@ -185,7 +185,7 @@ public class IntervalsFragment extends Fragment { } private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) { - boolean update = metricUnits != this.metricUnits || !selectedInterval.equals(this.selectedInterval); + boolean update = metricUnits != this.metricUnits || !selectedInterval.sameMultiplier(this.selectedInterval); this.metricUnits = metricUnits; this.selectedInterval = selectedInterval; diff --git a/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java b/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java index 315653077..e5fc9227c 100644 --- a/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java +++ b/src/main/java/de/dennisguse/opentracks/services/tasks/AnnouncementPeriodicTask.java @@ -178,19 +178,13 @@ public class AnnouncementPeriodicTask implements PeriodicTask { } String category = track.getCategory(); - //TODO Querying all TrackPoints all the time is inefficient; get infos from TrackRecordingService - TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null); - ArrayList trackPoints = new ArrayList<>(); - while (trackPointIterator.hasNext()) { - trackPoints.add(trackPointIterator.next()); - } - boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context); boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category); Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); + TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null); IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance); - intervalStatistics.addTrackPoints(trackPoints); + intervalStatistics.addTrackPoints(trackPointIterator); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval); diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java index d3ff8716e..f2a855d85 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java @@ -1,6 +1,5 @@ package de.dennisguse.opentracks.viewmodels; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.time.Duration; @@ -10,6 +9,7 @@ import java.util.List; import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; @@ -34,24 +34,28 @@ public class IntervalStatistics { intervalList.add(lastInterval); } - public void addTrackPoints(@NonNull List trackPoints) { - if (trackPoints.size() == 0) { - return; - } - + /** + * Complete intervals with the tracks points from the iterator. + * + * @param trackPointIterator + * @return the last track point's id used to compute the intervals. + */ + public TrackPoint.Id addTrackPoints(TrackPointIterator trackPointIterator) { boolean newIntervalAdded = false; - for (TrackPoint trackPoint : trackPoints) { + TrackPoint trackPoint = null; + + while (trackPointIterator.hasNext()) { + trackPoint = trackPointIterator.next(); trackStatisticsUpdater.addTrackPoint(trackPoint, minGPSDistance); if (trackStatisticsUpdater.getTrackStatistics().getTotalDistance().plus(interval.distance).greaterOrEqualThan(distanceInterval)) { - interval.update(trackStatisticsUpdater.getTrackStatistics(), trackPoint); + interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint); double adjustFactor = distanceInterval.dividedBy(interval.distance); Interval adjustedInterval = new Interval(interval); adjustedInterval.adjust(adjustFactor); - intervalList.remove(intervalList.size() - 1); - intervalList.add(adjustedInterval); + intervalList.set(intervalList.size() - 1, adjustedInterval); interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time)); trackStatisticsUpdater = new TrackStatisticsUpdater(); @@ -65,10 +69,12 @@ public class IntervalStatistics { } if (newIntervalAdded) { - lastInterval.update(trackStatisticsUpdater.getTrackStatistics(), null); + lastInterval.add(trackStatisticsUpdater.getTrackStatistics(), null); } else { lastInterval.set(trackStatisticsUpdater.getTrackStatistics()); } + + return trackPoint != null ? trackPoint.getId() : null; } public List getIntervalList() { @@ -145,7 +151,7 @@ public class IntervalStatistics { return loss_m; } - private void update(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) { + private void add(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) { distance = distance.plus(trackStatistics.getTotalDistance()); time = time.plus(trackStatistics.getTotalTime()); gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java index 0c8e53f1f..e49cd2149 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java @@ -21,6 +21,7 @@ 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.provider.ContentProviderUtils; +import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.util.PreferencesUtils; /** @@ -47,6 +48,9 @@ public class IntervalStatisticsModel extends AndroidViewModel { super(application); minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(PreferencesUtils.getSharedPreferences(application), application); contentResolver = getApplication().getContentResolver(); + handlerThread = new HandlerThread(TAG); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); } @Override @@ -54,6 +58,7 @@ public class IntervalStatisticsModel extends AndroidViewModel { super.onCleared(); if (trackPointsTableObserver != null) { contentResolver.unregisterContentObserver(trackPointsTableObserver); + trackPointsTableObserver = null; } if (handlerThread != null) { handlerThread.getLooper().quit(); @@ -75,25 +80,6 @@ public class IntervalStatisticsModel extends AndroidViewModel { loadIntervalStatistics(trackId); } - registerTrackPointsObserver(trackId); - - return intervalsLiveData; - } - - private void loadIntervalStatistics(Track.Id trackId) { - executor.execute(() -> { - ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication()); - List trackPoints = contentProviderUtils.getTrackPoints(trackId, lastTrackPointId); - lastTrackPointId = trackPoints.size() > 0 ? trackPoints.get(trackPoints.size() - 1).getId() : lastTrackPointId; - intervalStatistics.addTrackPoints(trackPoints); - intervalsLiveData.postValue(intervalStatistics.getIntervalList()); - }); - } - - private void registerTrackPointsObserver(Track.Id trackId) { - handlerThread = new HandlerThread(TAG); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); trackPointsTableObserver = new ContentObserver(handler) { @Override public void onChange(boolean selfChange, Uri uri) { @@ -101,6 +87,18 @@ public class IntervalStatisticsModel extends AndroidViewModel { } }; contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_TRACKID, false, trackPointsTableObserver); + + return intervalsLiveData; + } + + private void loadIntervalStatistics(Track.Id trackId) { + executor.execute(() -> { + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication()); + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, lastTrackPointId)) { + lastTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator); + intervalsLiveData.postValue(intervalStatistics.getIntervalList()); + } + }); } public void onPause() { @@ -145,7 +143,7 @@ public class IntervalStatisticsModel extends AndroidViewModel { .multipliedBy(multiplier); } - public boolean equals(IntervalOption intervalOption) { + public boolean sameMultiplier(IntervalOption intervalOption) { return intervalOption != null && this.multiplier == intervalOption.multiplier; } From 399f90a8f024156a9b13af213602d5c81813cfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xos=C3=A9=20M?= Date: Wed, 9 Jun 2021 03:33:35 +0000 Subject: [PATCH 3/4] Translated using Weblate (Galician) Currently translated at 100.0% (523 of 523 strings) Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/ --- src/main/res/values-gl/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 3998184bd..3f23cd8b0 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -34,7 +34,7 @@ limitations under the License. ¿Como mostrar a ruta nun mapa? Usa a icona do mapa pequeno. \n a) (actualizacións en directo) Instala OSMDashboard . Ofrece mapas en liña ou fóra de liña a través de OpenStreetMap . -\n \u0020En realidade, OSMDashboard é unha aplicación independente que só pode acceder (temporalmente) aos datos das pistas que se amosarán. +\n En realidade, OSMDashboard é unha aplicación independente que só pode acceder (temporalmente) aos datos das pistas que se amosarán. \n b) A través dunha aplicación de mapa normal (por exemplo, OsmAnd e MAPS.ME). Estas aplicacións precisan admitir o formato de ficheiro KMZ (só localizacións e marcas de tempo). De feito, unha aplicación ten que admitir ACTION_VIEW con MIME application/vnd.google-earth.kmz. ¿De que xeito garda OpenTracks as fotos tomadas como marcadores? Gárdanse no almacenaxe interno da app (só accesible desde OpenTracks). @@ -236,7 +236,7 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser Deter Traxecto - Importados: %1$d de %2$s de %3$s + Importados: %1$d de %2$s desde %3$s %1$s non existe Non hai ficheiros para importar. Conecta o teu dispositivo ao ordenador e coloca os ficheiros para importar en %1$s. Importar ficheiros @@ -422,7 +422,7 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser km/h m km - km/h + mph min/km min/km From 3b776407fa1084fbedb1551b9e4bb7aaf6304951 Mon Sep 17 00:00:00 2001 From: serpico Date: Tue, 15 Jun 2021 10:08:59 +0000 Subject: [PATCH 4/4] Translated using Weblate (French) Currently translated at 99.6% (521 of 523 strings) Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/ --- src/main/res/values-fr/strings.xml | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 17667e203..3239cfdb6 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -32,11 +32,11 @@ limitations under the License. Comment faire apparaître une trace sur une carte ? Utilisez la petite icône de carte. \n a) (mises à jour en direct) Installez OSMDashboard. Il fournit des cartes en ligne ou hors ligne via OpenStreetMap. -\n En fait, OSMDashboard est une application séparée qui ne peut accéder (temporairement) qu aux données des piste(s) qui vont être affichées. +\n En fait, OSMDashboard est une application séparée qui ne peut accéder (temporairement) qu aux données des trace(s) qui vont être affichées. \n b) Via une application cartographique normale (par exemple) , OsmAnd et MAPS.ME Ces applications doivent prendre en charge le format de fichier KMZ (uniquement les lieux et les horodatages). En fait, une application doit prendre en charge ACTION_VIEW avec MIME application/vnd.google-earth.kmz. Comment OpenTracks stocke-t-il les photos qui ont été prises comme marqueurs ? Elles sont stockées dans le stockage interne de lapplication (c est-à-dire uniquement accessible dans OpenTracks). - Comment puis-je exporter/partager des pistes enregistrées ? + Comment puis-je exporter/partager des traces enregistrées \? Les traces peuvent être exportées à partir des paramètres en tant que KMZ, KML ou GPX. KMZ/KML toutes les données ; GPX seulement les emplacements. Les traces partagées via l\'icône partager sont partagées au format KMZ. OpenTracks nécessite-t-il/utilise-t-il le service Google Play ? Comment OpenTracks enregistre-t-il les données ? @@ -276,7 +276,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Arrêter le GPS Afficher la photo Fréquence vocale - Reprendre la piste + Reprendre la trace Service enregistrement @@ -284,7 +284,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Parcours et repères Aucun parcours ni repère ne correspond à votre recherche - Dans la piste « %1$s ». + Dans la trace « %1$s ». Niveau de la batterie Cadences @@ -384,7 +384,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Graphique Statistiques Enregistrement d\'un nouveau parcours en cours - Reprenons maintenant la piste + Reprenons maintenant la trace Type d\'activité (exemple : randonnée) Type d\'activité @@ -536,20 +536,20 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Vitesse : %1$s Intervalles Il n\'y a pas encore d\'intervalle - Enregistrez votre première piste pour voir les statistiques agrégées + Enregistrez votre première trace pour voir les statistiques agrégées Temps au tour de %1$s Vitesse du tour de %1$s - Dernière piste exportée - Répertoire d\'exportation des pistes + Dernière trace exportée + Répertoire d\'exportation des traces Exportation instantanée après l\'entraînement Intervalles Perte - Empêcher la réimportation de pistes + Empêcher la réimportation de traces Nuit Jour Système Intervalle partagé - Exportation de la piste vers le stockage après la fin de l\'enregistrement + Exportation de la trace vers le stockage après la fin de l\'enregistrement Tenir pour faire une pause Maintenir pour arrêter Distanciation : %1$s @@ -561,7 +561,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Circonférence de roue (millimètre) L\'exportation du post-travail a échoué, veuillez vérifier le répertoire d\'exportation. le faire pour tous les conflits - Pistes non exportées + Traces non exportées Impossible d\'importer le fichier : %1$s Pas de doc.fichier kml trouvé dans KMZ %1$s : %2$s @@ -576,8 +576,8 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Rythme cardiaque moyen Cadence moyenne Cliquez sur retour pour annuler - Nouvelles pistes exportées - Pistes sautées + Nouvelles traces exportées + Traces sautées Erreurs Écraser Afficher les erreurs @@ -590,7 +590,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Format de fichier non pris en charge Erreur de l\'analyseur : %1$s Fichier %1$s importé - Vous avez défini empêcher la réimportation de la piste + Vous avez défini empêcher la réimportation de la trace Les fichiers importés Il existe déjà Erreur @@ -600,8 +600,8 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Format de fichier d\'exportation / partage Perte d\'altitude : %1$d m (%2$d ft) - La piste %1$s existe déjà dans le répertoire de destination. - Pistes écrasées + La trace %1$s existe déjà dans le répertoire de destination. + Traces écrasées Données de tiers %1$d/%2$d Service de suppression