IntervalsFragment doesn't use TrackDataHub anymore. This improve intervals performance. Fixes #808.

This commit is contained in:
Román Martínez
2021-06-03 17:16:03 +02:00
parent 4c68592a0c
commit 24e18347a6
12 changed files with 324 additions and 237 deletions
@@ -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:
@@ -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:
@@ -72,10 +72,6 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
}
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
if (intervalList == data && this.metricUnits == metricUnits && this.isReportSpeed == isReportSpeed) {
return null;
}
this.metricUnits = metricUnits;
this.isReportSpeed = isReportSpeed;
intervalList = data;
@@ -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;
@@ -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<IntervalStatisticsModel.IntervalOption> 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
* @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<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(metricUnits, selectedInterval);
LiveData<List<IntervalStatistics.Interval>> 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.sameMultiplier(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);
}
}
}
@@ -178,18 +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<TrackPoint> 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);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.one(isMetricUnits), minGPSDistance);
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits), minGPSDistance);
intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval);
@@ -1,6 +1,5 @@
package de.dennisguse.opentracks.viewmodels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
@@ -10,51 +9,72 @@ 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;
public class IntervalStatistics {
private final List<Interval> intervalList = new ArrayList<>();
private TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
private final List<Interval> 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<TrackPoint> trackPoints, Distance distanceInterval, Distance minGPSDistance) {
intervalList.clear();
public IntervalStatistics(Distance distanceInterval, Distance minGPSDistance) {
this.distanceInterval = distanceInterval;
this.minGPSDistance = minGPSDistance;
if (trackPoints.size() == 0) {
return;
}
interval = new Interval();
lastInterval = new Interval();
intervalList = new ArrayList<>();
intervalList.add(lastInterval);
}
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
Interval interval = new Interval();
/**
* 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;
TrackPoint trackPoint = null;
for (TrackPoint trackPoint : trackPoints) {
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.add(adjustedInterval);
intervalList.set(intervalList.size() - 1, 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.add(trackStatisticsUpdater.getTrackStatistics(), null);
} else {
lastInterval.set(trackStatisticsUpdater.getTrackStatistics());
}
return trackPoint != null ? trackPoint.getId() : null;
}
public List<Interval> getIntervalList() {
@@ -110,7 +130,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 +151,7 @@ public class IntervalStatistics {
return loss_m;
}
public 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;
@@ -141,5 +161,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;
}
}
}
@@ -1,19 +1,28 @@
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.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* This model is used to load intervals for a track.
@@ -21,65 +30,92 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class IntervalStatisticsModel extends AndroidViewModel {
private final List<TrackPoint> trackPoints = new ArrayList<>();
private static final String TAG = IntervalStatisticsModel.class.getSimpleName();
private MutableLiveData<List<IntervalStatistics.Interval>> 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();
handlerThread = new HandlerThread(TAG);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
public MutableLiveData<List<IntervalStatistics.Interval>> 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);
trackPointsTableObserver = null;
}
}
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<List<IntervalStatistics.Interval>> 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);
}
trackPointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
loadIntervalStatistics(trackId);
}
};
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() {
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 +143,10 @@ public class IntervalStatisticsModel extends AndroidViewModel {
.multipliedBy(multiplier);
}
public boolean sameMultiplier(IntervalOption intervalOption) {
return intervalOption != null && this.multiplier == intervalOption.multiplier;
}
@Override
public String toString() {
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.