From 22a669a5730188e0d800d9760449139bc2459f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n?= Date: Mon, 17 Aug 2020 18:02:31 +0200 Subject: [PATCH] Interval stats: added a new tab in TrackRecordingActivity that shows the intervals on live. --- .../opentracks/TrackRecordingActivity.java | 11 +- .../fragments/IntervalsRecordingFragment.java | 132 ++++++++++++++++++ .../viewmodels/IntervalStatisticsModel.java | 10 ++ .../opentracks/views/IntervalListView.java | 20 +-- .../views/IntervalReverseListView.java | 31 ++++ src/main/res/layout/intervals_recording.xml | 14 ++ src/main/res/values/strings.xml | 1 + 7 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/fragments/IntervalsRecordingFragment.java create mode 100644 src/main/java/de/dennisguse/opentracks/views/IntervalReverseListView.java create mode 100644 src/main/res/layout/intervals_recording.xml diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java index fda11f21f..30f718653 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java @@ -25,6 +25,7 @@ import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.fragments.ChartFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; +import de.dennisguse.opentracks.fragments.IntervalsRecordingFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceInterface; @@ -372,7 +373,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc @Override public int getCount() { - return 3; + return 4; } @NonNull @@ -382,8 +383,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc case 0: return new StatisticsRecordingFragment(); case 1: - return ChartFragment.newInstance(false); + return new IntervalsRecordingFragment(); case 2: + return ChartFragment.newInstance(false); + case 3: return ChartFragment.newInstance(true); default: throw new RuntimeException("There isn't Fragment associated with the position: " + position); @@ -396,8 +399,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc case 0: return getString(R.string.track_detail_stats_tab); case 1: - return getString(R.string.settings_chart_by_time); + return getString(R.string.track_detail_intervals_tab); case 2: + return getString(R.string.settings_chart_by_time); + case 3: return getString(R.string.settings_chart_by_distance); default: throw new RuntimeException("There isn't Fragment associated with the position: " + position); diff --git a/src/main/java/de/dennisguse/opentracks/fragments/IntervalsRecordingFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsRecordingFragment.java new file mode 100644 index 000000000..199862a87 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/fragments/IntervalsRecordingFragment.java @@ -0,0 +1,132 @@ +package de.dennisguse.opentracks.fragments; + +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.util.PreferencesUtils; +import de.dennisguse.opentracks.viewmodels.IntervalStatistics; +import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; +import de.dennisguse.opentracks.views.IntervalListView; +import de.dennisguse.opentracks.views.IntervalReverseListView; + +/** + * A fragment to display the intervals from recording track. + */ +public class IntervalsRecordingFragment extends Fragment implements IntervalListView.IntervalListListener { + + private static final String TAG = IntervalsRecordingFragment.class.getSimpleName(); + + private IntervalStatisticsModel viewModel; + private IntervalReverseListView intervalListView; + + private class IntervalRunner implements Runnable { + private boolean stopped = false; + + @Override + public void run() { + if (!stopped) { + updateIntervals(); + intervalHandler.postDelayed(intervalRunner, 5000); + } + } + + public void stop() { + stopped = true; + } + } + + private Handler intervalHandler = null; + private IntervalRunner intervalRunner = null; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.intervals_recording, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + intervalHandler = new Handler(); + + intervalListView = new IntervalReverseListView(getActivity(), this); + intervalListView.setId(View.generateViewId()); + LinearLayout linearLayout = view.findViewById(R.id.root_view); + linearLayout.removeAllViews(); + linearLayout.addView(intervalListView); + + viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class); + } + + @Override + public void onResume() { + super.onResume(); + if (intervalRunner == null) { + intervalRunner = new IntervalRunner(); + intervalRunner.run(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (intervalRunner != null) { + intervalRunner.stop(); + intervalRunner = null; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + intervalListView.destroy(); + intervalListView = null; + viewModel = null; + } + + /** + * Update intervals through {@link IntervalStatisticsModel} view model. + * + * @param interval intervals will split in this interval if not null. If it's null then view model will use the default one. + */ + private void updateIntervals(@Nullable IntervalStatisticsModel.IntervalOption interval) { + if (viewModel == null | intervalListView == null) { + return; + } + + long trackId = PreferencesUtils.getRecordingTrackId(getContext()); + viewModel.invalidate(); + LiveData liveData; + if (interval == null) { + liveData = viewModel.getIntervalStats(trackId); + } else { + liveData = viewModel.getIntervalStats(trackId, interval); + } + liveData.observe(getActivity(), intervalStatistics -> { + if (intervalStatistics != null) { + intervalListView.display(intervalStatistics.getIntervalList()); + } + }); + } + + private void updateIntervals() { + updateIntervals(null); + } + + @Override + public void intervalChanged(IntervalStatisticsModel.IntervalOption interval) { + updateIntervals(interval); + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java index ee37ac7d5..9170c09de 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatisticsModel.java @@ -15,6 +15,10 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.UnitConversions; +/** + * This view model is used to load intervals for a track. + * It uses a default interval but it can be set from outside to manage the interval length. + */ public class IntervalStatisticsModel extends AndroidViewModel { private MutableLiveData intervalStats; @@ -38,6 +42,9 @@ public class IntervalStatisticsModel extends AndroidViewModel { return getIntervalStats(trackId, interval); } + /** + * Call this method when you want to force the view model to re-load intervals. + */ public void invalidate() { intervalStats = null; } @@ -56,6 +63,9 @@ public class IntervalStatisticsModel extends AndroidViewModel { }).start(); } + /** + * Intervals length this view model support. + */ public enum IntervalOption { OPTION_1(1), OPTION_2(2), diff --git a/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java b/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java index 980e37fc3..8073eb1bf 100644 --- a/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java +++ b/src/main/java/de/dennisguse/opentracks/views/IntervalListView.java @@ -17,17 +17,21 @@ import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; +/** + * LinearLayout view used to build a list of intervals. + * See {@link IntervalStatisticsAdapter}. + */ public class IntervalListView extends LinearLayout { - private IntervalStatisticsAdapter adapter; - private LinearLayout linearLayoutIntervals; - private Spinner spinnerIntervals; - private TextView spinnerIntervalsUnit; + protected IntervalStatisticsAdapter adapter; + protected LinearLayout linearLayoutIntervals; + protected Spinner spinnerIntervals; + protected TextView spinnerIntervalsUnit; - private Context context; - private IntervalListListener listener; + protected Context context; + protected IntervalListListener listener; - private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> { + protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (spinnerIntervalsUnit != null) { spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(context) ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile)); @@ -43,7 +47,7 @@ public class IntervalListView extends LinearLayout { init(); } - private void init() { + protected void init() { inflate(getContext(), R.layout.interval_list_view, this); linearLayoutIntervals = findViewById(R.id.interval_list); spinnerIntervals = findViewById(R.id.spinner_intervals); diff --git a/src/main/java/de/dennisguse/opentracks/views/IntervalReverseListView.java b/src/main/java/de/dennisguse/opentracks/views/IntervalReverseListView.java new file mode 100644 index 000000000..7c22966cb --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/views/IntervalReverseListView.java @@ -0,0 +1,31 @@ +package de.dennisguse.opentracks.views; + +import android.content.Context; +import android.view.View; + +import java.util.List; + +import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter; +import de.dennisguse.opentracks.viewmodels.IntervalStatistics; + +/** + * LinearLayout view used to build a list of intervals in a reverse mode, the last one will appear in the first position on the LinearLayout. + * This class is an specialization of {@link IntervalListView} that display the views contained in the LinearLayout in a reverse mode. + */ +public class IntervalReverseListView extends IntervalListView { + + public IntervalReverseListView(Context context, IntervalListListener listener) { + super(context, listener); + } + + public void display(List intervalList) { + if (intervalList != null) { + adapter = new IntervalStatisticsAdapter(getContext(), intervalList); + linearLayoutIntervals.removeAllViews(); + for (int i = 0; i < adapter.getCount(); i++) { + View intervalView = adapter.getView(i, null, linearLayoutIntervals); + linearLayoutIntervals.addView(intervalView, 0); + } + } + } +} diff --git a/src/main/res/layout/intervals_recording.xml b/src/main/res/layout/intervals_recording.xml new file mode 100644 index 000000000..93baa1b2e --- /dev/null +++ b/src/main/res/layout/intervals_recording.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ccca985f0..17379ff5e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -432,6 +432,7 @@ limitations under the License. Chart Stats + Intervals Now recording a new track Now resuming the track