TrackDetailActivity: show heart rate.

This commit is contained in:
Dennis Guse
2019-07-20 12:11:48 +02:00
parent 09df2dd20f
commit cf0419f27b
10 changed files with 113 additions and 32 deletions
@@ -64,6 +64,14 @@ public final class SensorDataSet {
return creationTimestamp; return creationTimestamp;
} }
/**
* Is the data recent considering the current time.
* @param maxAge the maximal age in milliseconds.
*/
public boolean isRecent(long maxAge) {
return creationTimestamp + maxAge > System.currentTimeMillis();
}
public boolean hasBatteryLevel() { public boolean hasBatteryLevel() {
return batteryLevel != DATA_UNAVAILABLE; return batteryLevel != DATA_UNAVAILABLE;
} }
@@ -248,11 +248,9 @@ public class MarkerDetailFragment extends Fragment {
setName(R.id.marker_detail_waypoint_name, hasPhoto); setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category); TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView( ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView() TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
.findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto); ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto); setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
@@ -19,6 +19,7 @@ package com.google.android.apps.mytracks.fragments;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -36,11 +37,15 @@ import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StatsUtils; import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils; import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -59,8 +64,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private Handler handlerUpdateUI; private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null; private Location lastLocation = null;
private TripStatistics lastTripStatistics = null; private TripStatistics lastTripStatistics = null;
private SensorDataSet sensorDataSet = null;
private String category = ""; private String category = "";
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT; private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
@@ -70,12 +78,15 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isResumed() && isSelectedTrackRecording()) { if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) { if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime()); StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
updateSensorDataUI();
} }
handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND); handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND);
} }
} }
}; };
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false); return inflater.inflate(R.layout.stats, container, false);
@@ -85,6 +96,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
handlerUpdateUI = new Handler(); handlerUpdateUI = new Handler();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
TrackRecordingServiceConnectionUtils.startConnection(getContext(), trackRecordingServiceConnection);
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon); Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), "")); activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
@@ -125,6 +138,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
handlerUpdateUI.removeCallbacks(updateTotalTime); handlerUpdateUI.removeCallbacks(updateTotalTime);
} }
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override @Override
public void onTrackUpdated(final Track track) { public void onTrackUpdated(final Track track) {
if (isResumed()) { if (isResumed()) {
@@ -286,6 +305,22 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused(); return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
} }
/**
* Tries to fetch most recent {@link SensorDataSet} {@link com.google.android.apps.mytracks.services.sensors.SensorManager}.
*/
private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService == null) {
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
sensorDataSet = null;
} else {
//TODO sensorState = trackRecordingService.getSensorState();
sensorDataSet = trackRecordingService.getSensorData();
}
StatsUtils.setSensorData(getActivity(), getActivity(), sensorDataSet, isSelectedTrackRecording());
}
/** /**
* Updates the UI. * Updates the UI.
*/ */
@@ -293,5 +328,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
String trackIconValue = TrackIconUtils.getIconValue(activity, category); String trackIconValue = TrackIconUtils.getIconValue(activity, category);
StatsUtils.setTripStatisticsValues(activity, activity, null, lastTripStatistics, trackIconValue); StatsUtils.setTripStatisticsValues(activity, activity, null, lastTripStatistics, trackIconValue);
StatsUtils.setLocationValues(activity, activity, null, lastLocation, isSelectedTrackRecording()); StatsUtils.setLocationValues(activity, activity, null, lastLocation, isSelectedTrackRecording());
updateSensorDataUI();
} }
} }
@@ -31,9 +31,10 @@ import java.util.TimerTask;
*/ */
public abstract class SensorManager { public abstract class SensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE = 5000;
public static final long MAX_SENSOR_STATE_AGE = 20000;
private static final String TAG = SensorManager.class.getSimpleName(); private static final String TAG = SensorManager.class.getSimpleName();
private static final long MAX_SENSOR_DATE_SET_AGE = 5000;
private static final long MAX_SENSOR_STATE_AGE = 20000;
private static final int RETRY_PERIOD = 20000; private static final int RETRY_PERIOD = 20000;
private SensorState sensorState = SensorState.NONE; private SensorState sensorState = SensorState.NONE;
@@ -132,6 +133,6 @@ public abstract class SensorManager {
if (sensorDataSet == null) { if (sensorDataSet == null) {
return false; return false;
} }
return (System.currentTimeMillis() - sensorDataSet.getCreationTime()) < MAX_SENSOR_DATE_SET_AGE; return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE);
} }
} }
@@ -23,6 +23,8 @@ import android.view.View;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -51,8 +53,7 @@ public class StatsUtils {
* @param location the location * @param location the location
* @param isRecording true if recording * @param isRecording true if recording
*/ */
public static void setLocationValues( public static void setLocationValues(Context context, Activity activity, View view, Location location, boolean isRecording) {
Context context, Activity activity, View view, Location location, boolean isRecording) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context); boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
@@ -61,29 +62,22 @@ public class StatsUtils {
speed.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); speed.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) { if (isRecording) {
double value = isRecording && location != null && location.hasSpeed() ? location.getSpeed() double value = isRecording && location != null && location.hasSpeed() ? location.getSpeed() : Double.NaN;
: Double.NaN; setSpeed(context, speed, R.string.stats_speed, R.string.stats_pace, value, metricUnits, reportSpeed);
setSpeed(context, speed, R.string.stats_speed, R.string.stats_pace, value, metricUnits,
reportSpeed);
} }
// Set elevation // Set elevation
boolean showGradeElevation = PreferencesUtils.getBoolean( boolean showGradeElevation = PreferencesUtils.getBoolean(context, R.string.stats_show_grade_elevation_key, PreferencesUtils.STATS_SHOW_GRADE_ELEVATION_DEFAULT) && isRecording;
context, R.string.stats_show_grade_elevation_key,
PreferencesUtils.STATS_SHOW_GRADE_ELEVATION_DEFAULT) && isRecording;
View elevation = getView(activity, view, R.id.stats_elevation); View elevation = getView(activity, view, R.id.stats_elevation);
elevation.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE); elevation.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) { if (showGradeElevation) {
double altitude = location != null && location.hasAltitude() ? location.getAltitude() double altitude = location != null && location.hasAltitude() ? location.getAltitude() : Double.NaN;
: Double.NaN;
setElevationValue(context, elevation, -1, altitude, metricUnits); setElevationValue(context, elevation, -1, altitude, metricUnits);
} }
// Set coordinate // Set coordinate
boolean showCoordinate = PreferencesUtils.getBoolean( boolean showCoordinate = isRecording && PreferencesUtils.getBoolean(context, R.string.stats_show_coordinate_key, PreferencesUtils.STATS_SHOW_COORDINATE_DEFAULT);
context, R.string.stats_show_coordinate_key, PreferencesUtils.STATS_SHOW_COORDINATE_DEFAULT)
&& isRecording;
View coordinateSeparator = getView(activity, view, R.id.stats_coordinate_separator); View coordinateSeparator = getView(activity, view, R.id.stats_coordinate_separator);
View coordinateContainer = getView(activity, view, R.id.stats_coordinate_container); View coordinateContainer = getView(activity, view, R.id.stats_coordinate_container);
@@ -95,10 +89,19 @@ public class StatsUtils {
if (showCoordinate) { if (showCoordinate) {
double latitude = location != null ? location.getLatitude() : Double.NaN; double latitude = location != null ? location.getLatitude() : Double.NaN;
double longitude = location != null ? location.getLongitude() : Double.NaN; double longitude = location != null ? location.getLongitude() : Double.NaN;
setCoordinateValue( setCoordinateValue(context, getView(activity, view, R.id.stats_latitude), R.string.stats_latitude, latitude);
context, getView(activity, view, R.id.stats_latitude), R.string.stats_latitude, latitude); setCoordinateValue(context, getView(activity, view, R.id.stats_longitude), R.string.stats_longitude, longitude);
setCoordinateValue(context, getView(activity, view, R.id.stats_longitude), }
R.string.stats_longitude, longitude); }
public static void setSensorData(Context context, Activity activity, SensorDataSet sensorDataSet, boolean isRecording) {
{
// SensorDataSet: heart rate
View view = getView(activity, null, R.id.stats_sensor_heart_rate_value);
view.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
setHeartRateValue(context, view, sensorDataSet);
}
} }
} }
@@ -109,8 +112,7 @@ public class StatsUtils {
* @param totalTime the total time * @param totalTime the total time
*/ */
public static void setTotalTimeValue(Activity activity, long totalTime) { public static void setTotalTimeValue(Activity activity, long totalTime) {
setTimeValue(activity, activity.findViewById(R.id.stats_total_time), R.string.stats_total_time, setTimeValue(activity, activity.findViewById(R.id.stats_total_time), R.string.stats_total_time, totalTime);
totalTime);
} }
/** /**
@@ -281,6 +283,21 @@ public class StatsUtils {
setItem(context, view, labelId, value, null); setItem(context, view, labelId, value, null);
} }
/**
* Sets a heart rate value (sensor)
*
* @param context the context
* @param view the containing view
* @param sensorDataSet the sensorDataSet
*/
private static void setHeartRateValue(Context context, View view, SensorDataSet sensorDataSet) {
String heartRate = context.getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.hasHeartRate() && sensorDataSet.isRecent(SensorManager.MAX_SENSOR_DATE_SET_AGE)) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
}
setItem(context, view, R.string.sensor_state_heart_rate, heartRate, context.getString(R.string.sensor_unit_beats_per_minute));
}
/** /**
* Sets an item. * Sets an item.
* *
@@ -328,6 +345,7 @@ public class StatsUtils {
* @param view the containing view * @param view the containing view
* @param id the id * @param id the id
*/ */
//TODO What is the difference between the two cases? Is it (still) necessary?
private static View getView(Activity activity, View view, int id) { private static View getView(Activity activity, View view, int id) {
if (activity != null) { if (activity != null) {
return activity.findViewById(id); return activity.findViewById(id);
@@ -152,7 +152,11 @@ public class StringUtils {
} }
public static String formatDecimal(double value) { public static String formatDecimal(double value) {
String result = String.format(Locale.getDefault(), "%1$,.2f", value); return StringUtils.formatDecimal(value, 2);
}
public static String formatDecimal(double value, int decimalPlaces) {
String result = String.format(Locale.getDefault(), "%1$,." + decimalPlaces +"f", value);
return result.replaceAll("[0]*$", "").replaceAll("\\.$", ""); return result.replaceAll("[0]*$", "").replaceAll("\\.$", "");
} }
@@ -133,8 +133,7 @@ public class TrackRecordingServiceConnectionUtils {
* @param trackRecordingServiceConnection the track recording service * @param trackRecordingServiceConnection the track recording service
* connection * connection
*/ */
public static void startConnection( public static void startConnection(Context context, TrackRecordingServiceConnection trackRecordingServiceConnection) {
Context context, TrackRecordingServiceConnection trackRecordingServiceConnection) {
trackRecordingServiceConnection.bindIfStarted(); trackRecordingServiceConnection.bindIfStarted();
if (!isRecordingServiceRunning(context)) { if (!isRecordingServiceRunning(context)) {
resetRecordingState(context); resetRecordingState(context);
@@ -134,4 +134,19 @@ limitations under the License.
android:id="@+id/stats_longitude" android:id="@+id/stats_longitude"
layout="@layout/stats_small_item" /> layout="@layout/stats_small_item" />
</LinearLayout> </LinearLayout>
<!-- Sensor data-->
<View
android:id="@+id/stats_sensor_separator"
style="@style/StatsHorizontalLine" />
<LinearLayout
android:id="@+id/stats_sensor_container"
android:layout_height="wrap_content"
android:layout_width="match_parent" >
<include
android:id="@+id/stats_sensor_heart_rate_value"
layout="@layout/stats_small_item" />
<include
android:id="@+id/stats_sensor_PLACEHOLDER"
layout="@layout/stats_small_item" />
</LinearLayout>
</merge> </merge>
@@ -81,6 +81,8 @@ Google은 이용자에 대한 위치정보의 이용∙제공사실을 위치정
<string name="sensor_type_value_none">NONE</string> <string name="sensor_type_value_none">NONE</string>
<string name="sensor_type_value_polar">POLAR</string> <string name="sensor_type_value_polar">POLAR</string>
<string name="sensor_type_value_zephyr">ZEPHYR</string> <string name="sensor_type_value_zephyr">ZEPHYR</string>
<string name="sensor_unit_rounds_per_minute">RPM</string>
<string name="sensor_unit_beats_per_minute">BPM</string>
<string name="settings_map_track_color_mode_dynamic_value">DYNAMIC</string> <string name="settings_map_track_color_mode_dynamic_value">DYNAMIC</string>
<string name="settings_map_track_color_mode_fixed_value">FIXED</string> <string name="settings_map_track_color_mode_fixed_value">FIXED</string>
<string name="settings_map_track_color_mode_single_value">SINGLE</string> <string name="settings_map_track_color_mode_single_value">SINGLE</string>
+1 -1
View File
@@ -1392,7 +1392,7 @@ limitations under the License.
</string> </string>
<string name="sensor_state_heart_rate_value" <string name="sensor_state_heart_rate_value"
translation_description="In the 'Sensor state' page, the value for the 'Heart rate'. E.g., translation_description="In the 'Sensor state' page, the value for the 'Heart rate'. E.g.,
'Hear rate: 100 bpm'. 'Heart rate: 100 bpm'.
%1$d a number. E.g., '100'."> %1$d a number. E.g., '100'.">
%1$d bpm %1$d bpm