/* * Copyright 2008 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.location.Location; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import java.util.EnumSet; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataListener; import de.dennisguse.opentracks.content.TrackDataType; import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.UnitConversions; /** * A fragment to display track statistics to the user. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class StatsFragment extends Fragment implements TrackDataListener { public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName(); private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND; private TrackDataHub trackDataHub; private Handler handlerUpdateUI; private TrackRecordingServiceConnection trackRecordingServiceConnection; private Location lastLocation = null; private TripStatistics lastTripStatistics = null; private String category = ""; private int recordingGpsAccuracy; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.stats, container, false); } /* Views */ private View sensorContainerView; private TextView heartRateValueView; private TextView heartRateSensorView; private TextView totalTimeValueView; private final Runnable updateUIeachSecond = new Runnable() { public void run() { if (isResumed() && isSelectedTrackRecording()) { if (!isSelectedTrackPaused() && lastTripStatistics != null) { updateTotalTime(); updateSensorDataUI(); } handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL); } } }; private TextView distanceValue; private TextView distanceUnit; private View activityLabel; private Spinner activitySpinner; private TextView movingTimeValue; private TextView speedAvgLabel; private TextView speedAvgValue; private TextView speedAvgUnit; private TextView speedMaxLabel; private TextView speedMaxValue; private TextView speedMaxUnit; private TextView speedMovingLabel; private TextView speedMovingValue; private TextView speedMovingUnit; private View gradeElevationSeparator; private View gradeElevationContainer; private TextView elevationMinValue; private TextView elevationMinUnit; private TextView elevationMaxValue; private TextView elevationMaxUnit; private View speedContainer; private TextView speedLabel; private TextView speedValue; private TextView speedUnit; private View elevationContainer; private TextView elevationValue; private TextView elevationUnit; private View coordinateSeparator; private View coordinateContainer; private TextView latitudeValue; private TextView longitudeValue; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); sensorContainerView = view.findViewById(R.id.stats_sensor_container); heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value); heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value); totalTimeValueView = view.findViewById(R.id.stats_total_time_value); distanceValue = view.findViewById(R.id.stats_distance_value); distanceUnit = view.findViewById(R.id.stats_distance_unit); activityLabel = view.findViewById(R.id.stats_activity_type_label); activitySpinner = view.findViewById(R.id.stats_activity_type_icon); movingTimeValue = view.findViewById(R.id.stats_moving_time_value); speedAvgLabel = view.findViewById(R.id.stats_average_speed_label); speedAvgValue = view.findViewById(R.id.stats_average_speed_value); speedAvgUnit = view.findViewById(R.id.stats_average_speed_unit); speedMaxLabel = view.findViewById(R.id.stats_max_speed_label); speedMaxValue = view.findViewById(R.id.stats_max_speed_value); speedMaxUnit = view.findViewById(R.id.stats_max_speed_unit); speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label); speedMovingValue = view.findViewById(R.id.stats_moving_speed_value); speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit); gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator); gradeElevationContainer = view.findViewById(R.id.stats_elevation_container); elevationMinValue = view.findViewById(R.id.stats_elevation_min_value); elevationMinUnit = view.findViewById(R.id.stats_elevation_min_unit); elevationMaxValue = view.findViewById(R.id.stats_elevation_max_value); elevationMaxUnit = view.findViewById(R.id.stats_elevation_max_unit); speedContainer = view.findViewById(R.id.stats_speed); speedLabel = view.findViewById(R.id.stats_speed_label); speedValue = view.findViewById(R.id.stats_speed_value); speedUnit = view.findViewById(R.id.stats_speed_unit); elevationContainer = view.findViewById(R.id.stats_elevation); elevationValue = view.findViewById(R.id.stats_elevation_current_value); elevationUnit = view.findViewById(R.id.stats_elevation_current_unit); coordinateSeparator = view.findViewById(R.id.stats_coordinate_separator); coordinateContainer = view.findViewById(R.id.stats_coordinate_container); latitudeValue = view.findViewById(R.id.stats_latitude_value); longitudeValue = view.findViewById(R.id.stats_longitude_value); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); recordingGpsAccuracy = Integer.parseInt(getContext().getResources().getString(R.string.recording_gps_accuracy_default)); handlerUpdateUI = new Handler(); Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon); activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), "")); activityTypeIcon.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { ((TrackDetailActivity) getActivity()).chooseActivityType(category); } return true; } }); activityTypeIcon.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { ((TrackDetailActivity) getActivity()).chooseActivityType(category); } return true; } }); } @Override public void onResume() { super.onResume(); resumeTrackDataHub(); trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null); trackRecordingServiceConnection.startConnection(getContext()); handlerUpdateUI.post(updateUIeachSecond); } @Override public void onPause() { super.onPause(); pauseTrackDataHub(); handlerUpdateUI.removeCallbacks(updateUIeachSecond); } @Override public void onStop() { super.onStop(); trackRecordingServiceConnection.unbind(); trackRecordingServiceConnection = null; } @Override public void onDestroyView() { super.onDestroyView(); sensorContainerView = null; heartRateValueView = null; heartRateSensorView = null; totalTimeValueView = null; distanceValue = null; distanceUnit = null; activityLabel = null; activitySpinner = null; movingTimeValue = null; speedAvgLabel = null; speedAvgValue = null; speedAvgUnit = null; speedMaxLabel = null; speedMaxValue = null; speedMaxUnit = null; speedMovingLabel = null; speedMovingValue = null; speedMovingUnit = null; gradeElevationSeparator = null; gradeElevationContainer = null; elevationMinValue = null; elevationMinUnit = null; elevationMaxValue = null; elevationMaxUnit = null; speedContainer = null; speedLabel = null; speedValue = null; speedUnit = null; elevationContainer = null; elevationValue = null; elevationUnit = null; coordinateSeparator = null; coordinateContainer = null; latitudeValue = null; longitudeValue = null; } @Override public void onTrackUpdated(final Track track) { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { lastTripStatistics = track != null ? track.getTripStatistics() : null; category = track != null ? track.getCategory() : ""; updateUI(); } } }); } } @Override public void clearTrackPoints() { lastLocation = null; } @Override public void onSampledInTrackPoint(Location location) { lastLocation = location; } @Override public void onSampledOutTrackPoint(Location location) { lastLocation = location; } @Override public void onNewTrackPointsDone() { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { if (!isSelectedTrackRecording() || isSelectedTrackPaused()) { lastLocation = null; } if (lastLocation != null) { boolean hasFix = !LocationUtils.isLocationOld(lastLocation); boolean hasGoodFix = lastLocation.hasAccuracy() && lastLocation.getAccuracy() < recordingGpsAccuracy; if (!hasFix || !hasGoodFix) { lastLocation = null; } } setLocationValues(); } } }); } } @Override public void clearWaypoints() { // We don't care. } @Override public void onNewWaypoint(Waypoint wpt) { // We don't care. } @Override public void onNewWaypointsDone() { // We don't care. } @Override public boolean onMetricUnitsChanged(final boolean metric) { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { updateUI(); } } }); } return true; } @Override public boolean onReportSpeedChanged(final boolean speed) { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (isResumed()) { updateUI(); } } }); } return true; } @Override public boolean onRecordingGpsAccuracy(int newValue) { recordingGpsAccuracy = newValue; return false; } @Override public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) { // We don't care. return false; } /** * 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, EnumSet.of(TrackDataType.TRACKS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE)); } /** * 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(); } /** * Returns true if the selected track is paused. * Needs to be synchronized because trackDataHub can be accessed by multiple threads. */ private synchronized boolean isSelectedTrackPaused() { return trackDataHub != null && trackDataHub.isSelectedTrackPaused(); } private void updateUI() { updateStats(); setLocationValues(); updateSensorDataUI(); } /** * Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}. */ private void updateSensorDataUI() { ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); SensorDataSet sensorDataSet = null; if (trackRecordingService == null) { Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service."); } else { //TODO sensorState = trackRecordingService.getSensorState(); sensorDataSet = trackRecordingService.getSensorData(); } setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording()); } private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) { // heart rate int isVisible = View.VISIBLE; if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) { isVisible = View.INVISIBLE; } sensorContainerView.setVisibility(isVisible); if (isRecording) { String heartRate = getContext().getString(R.string.value_unknown); String sensorName = getContext().getString(R.string.value_unknown); if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) { sensorName = sensorDataSet.getSensorName(); if (sensorDataSet.hasHeartRate()) { heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0); } } heartRateSensorView.setText(sensorName); heartRateValueView.setText(heartRate); } } private void updateStats() { String trackIconValue = TrackIconUtils.getIconValue(getContext(), category); boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext()); boolean isRecording = isSelectedTrackRecording(); // Set total distance { double totalDistance = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getTotalDistance(); Pair parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits); distanceValue.setText(parts.first); distanceUnit.setText(parts.second); } // Set activity type { activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE); activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE); activitySpinner.setEnabled(isRecording); if (trackIconValue != null) { TrackIconUtils.setIconSpinner(activitySpinner, trackIconValue); } } // Set time if (lastTripStatistics != null) { movingTimeValue.setText(StringUtils.formatElapsedTime(lastTripStatistics.getMovingTime())); updateTotalTime(); } // Set average speed/pace { double speed = lastTripStatistics != null ? lastTripStatistics.getAverageSpeed() : Double.NaN; speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace); Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); speedAvgValue.setText(parts.first); speedAvgUnit.setText(parts.second); } // Set max speed/pace { double speed = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxSpeed(); speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace); Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); speedMaxValue.setText(parts.first); speedMaxUnit.setText(parts.second); } // Set moving speed/pace { double speed = lastTripStatistics != null ? lastTripStatistics.getAverageMovingSpeed() : Double.NaN; speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace); Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); speedMovingValue.setText(parts.first); speedMovingUnit.setText(parts.second); } // Set elevation { boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext()); gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE); gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE); if (showElevation) { { double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMinElevation(); Pair parts = StringUtils.formatElevation(getContext(), elevation, metricUnits); elevationMinValue.setText(parts.first); elevationMinUnit.setText(parts.second); } { double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxElevation(); Pair parts = StringUtils.formatElevation(getContext(), elevation, metricUnits); elevationMaxValue.setText(parts.first); elevationMaxUnit.setText(parts.second); } } } } private void updateTotalTime() { long totalTime; if (isSelectedTrackRecording()) { totalTime = System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime(); } else { totalTime = lastTripStatistics.getTotalTime(); } totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime)); } private void setLocationValues() { boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext()); boolean isRecording = isSelectedTrackRecording(); // Set speed/pace speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); if (isRecording) { speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace); double speed = lastLocation != null && lastLocation.hasSpeed() ? lastLocation.getSpeed() : Double.NaN; Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); speedValue.setText(parts.first); speedUnit.setText(parts.second); } // Set elevation boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext()); elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE); if (showGradeElevation) { double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN; Pair parts = StringUtils.formatElevation(getContext(), altitude, metricUnits); elevationValue.setText(parts.first); elevationUnit.setText(parts.second); } // Set coordinate boolean showCoordinate = isRecording && PreferencesUtils.isStatsShowCoordinate(getContext()); coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE); coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE); if (showCoordinate) { double latitude = lastLocation != null ? lastLocation.getLatitude() : Double.NaN; String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude); latitudeValue.setText(latitudeText); double longitude = lastLocation != null ? lastLocation.getLongitude() : Double.NaN; String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude); longitudeValue.setText(longitudeText); } } }