/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package de.dennisguse.opentracks.fragments; import android.content.SharedPreferences; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import java.util.ArrayList; import java.util.List; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.chart.ChartPoint; import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataListener; import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.PreferencesUtils; /** * A fragment to display track chart to the user. * ChartFragment uses a {@link TrackStatisticsUpdater} internally and recomputes the {@link TrackStatistics} from the beginning. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class ChartFragment extends Fragment implements TrackDataListener { private static final String KEY_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance"; public static Fragment newInstance(boolean chartByDistance) { Bundle bundle = new Bundle(); bundle.putBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance); ChartFragment chartFragment = new ChartFragment(); chartFragment.setArguments(bundle); return chartFragment; } private final List pendingPoints = new ArrayList<>(); private TrackDataHub trackDataHub; // Stats gathered from the received data private TrackStatisticsUpdater trackStatisticsUpdater; private long startTime; //TODO Why is this needed? private int recordingDistanceInterval; // Modes of operation private boolean chartByDistance; // UI elements private ChartView chartView; private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) { boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); if (metricUnits != chartView.getMetricUnits()) { chartView.setMetricUnits(metricUnits); runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { chartView.requestLayout(); } } }); } } if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext()); if (reportSpeed != chartView.getReportSpeed()) { chartView.setReportSpeed(reportSpeed); chartView.applyReportSpeed(); runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { chartView.requestLayout(); } } }); } } if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) { recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(getContext()); } } }; /** * A runnable that will set the orange pointer as appropriate and redraw. */ private final Runnable updateChart = new Runnable() { @Override public void run() { if (!isResumed() || trackDataHub == null) { return; } chartView.setShowPointer(isSelectedTrackRecording()); chartView.invalidate(); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true); recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext()); // Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume. chartView = new ChartView(getContext(), chartByDistance); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.chart, container, false); } @Override public void onStart() { super.onStart(); ViewGroup layout = getView().findViewById(R.id.chart_view_layout); LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); layout.addView(chartView, layoutParams); } @Override public void onResume() { super.onResume(); resumeTrackDataHub(); PreferencesUtils.register(getContext(), sharedPreferenceChangeListener); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); checkChartSettings(); getActivity().runOnUiThread(updateChart); } @Override public void onPause() { super.onPause(); pauseTrackDataHub(); PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener); } @Override public void onStop() { super.onStop(); ViewGroup layout = getView().findViewById(R.id.chart_view_layout); layout.removeView(chartView); } @Override public void onTrackUpdated(Track track) { if (isResumed()) { if (track == null || track.getTrackStatistics() == null) { startTime = -1L; return; } startTime = track.getTrackStatistics().getStartTime_ms(); } } @Override public void clearTrackPoints() { if (isResumed()) { trackStatisticsUpdater = startTime != -1L ? new TrackStatisticsUpdater(startTime) : null; pendingPoints.clear(); chartView.reset(); runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { chartView.resetScroll(); } } }); } } @Override public void onSampledInTrackPoint(TrackPoint trackPoint) { if (isResumed()) { pendingPoints.add(createPendingPoint(trackPoint)); } } @Override public void onSampledOutTrackPoint(TrackPoint trackPoint) { if (isResumed()) { if (trackStatisticsUpdater != null) { trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); } } } @Override public void onNewTrackPointsDone() { if (isResumed()) { chartView.addChartPoints(pendingPoints); pendingPoints.clear(); runOnUiThread(updateChart); } } @Override public void clearWaypoints() { if (isResumed()) { chartView.clearWaypoints(); } } @Override public void onNewWaypoint(Waypoint waypoint) { if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) { chartView.addWaypoint(waypoint); } } @Override public void onNewWaypointsDone() { if (isResumed()) { runOnUiThread(updateChart); } } /** * Checks the chart settings. */ private void checkChartSettings() { boolean needUpdate = chartView.applyReportSpeed(); if (needUpdate) { chartView.postInvalidate(); } } /** * Resumes the trackDataHub. * Needs to be synchronized because trackDataHub can be accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, true, true, true, true); } /** * Pauses the trackDataHub. * Needs to be synchronized because trackDataHub can be accessed by multiple threads. */ private synchronized void pauseTrackDataHub() { trackDataHub.unregisterTrackDataListener(this); trackDataHub = null; } /** * Returns true if the selected track is recording. * Needs to be synchronized because trackDataHub can be accessed by multiple threads. */ private synchronized boolean isSelectedTrackRecording() { return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); } /** * Runs a runnable on the UI thread if possible. * * @param runnable the runnable */ private void runOnUiThread(Runnable runnable) { FragmentActivity fragmentActivity = getActivity(); if (fragmentActivity != null) { fragmentActivity.runOnUiThread(runnable); } } @VisibleForTesting ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) { trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits()); } @VisibleForTesting void setTrackStatisticsUpdater(long time) { trackStatisticsUpdater = new TrackStatisticsUpdater(time); } @VisibleForTesting void setChartView(ChartView view) { chartView = view; } @VisibleForTesting void setMetricUnits(boolean value) { chartView.setMetricUnits(value); } @VisibleForTesting void setReportSpeed(boolean value) { chartView.setReportSpeed(value); } @VisibleForTesting void setChartByDistance(boolean value) { chartByDistance = value; } }