From 0e0958c5e8ab57ea099f73a53eee8e31dd5360fe Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 29 Dec 2024 12:38:15 +0100 Subject: [PATCH] Announcements: add current heartrate. Fixes #1635. --- .../VoiceAnnouncementUtilsTest.java | 93 +++++++++--- .../sensors/sensorData/SensorDataSet.java | 2 +- .../services/TrackRecordingService.java | 2 +- .../VoiceAnnouncementManager.java | 9 +- .../announcement/VoiceAnnouncementUtils.java | 132 ++++++++++-------- .../opentracks/settings/PreferencesUtils.java | 17 ++- src/main/res/values/settings.xml | 2 + src/main/res/values/strings.xml | 1 + src/main/res/xml/settings_announcements.xml | 5 + 9 files changed, 174 insertions(+), 89 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java index c95630fd0..6886d9dd3 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java @@ -15,6 +15,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.time.Duration; import java.time.Instant; @@ -29,6 +30,7 @@ import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.stats.SensorStatistics; @@ -48,6 +50,7 @@ public class VoiceAnnouncementUtilsTest { public void setUp() { contentProviderUtils = new ContentProviderUtils(context); + PreferencesUtils.setVoiceAnnounceHeartRateCurrent(false); PreferencesUtils.setVoiceAnnounceLapHeartRate(false); PreferencesUtils.setVoiceAnnounceAverageHeartRate(false); PreferencesUtils.setVoiceAnnounceTotalDistance(true); @@ -74,8 +77,11 @@ public void getAnnouncement_metric_speed() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Average moving speed 18.4 kilometers per hour.", announcement); @@ -94,8 +100,11 @@ public void getAnnouncement_metric_speed_rounding_check() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 1 second. Average moving speed 20.0 kilometers per hour.", announcement); @@ -114,8 +123,11 @@ public void getAnnouncement_metric_distance_rounding_check() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour. Average moving speed 20.0 kilometers per hour.", announcement); @@ -134,8 +146,11 @@ public void getAnnouncement_metric_distance_rounding_check_two() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 19.9 kilometers. 1 hour. Average moving speed 19.9 kilometers per hour.", announcement); @@ -156,11 +171,14 @@ public void getAnnouncement_withInterval_metric_speed() { lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); } + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + Track track = new Track(); track.setTrackStatistics(stats); // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, lastInterval, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, null).toString(); // then assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement); @@ -179,8 +197,11 @@ public void getAnnouncement_metric_pace() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, false, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, null, null).toString(); // then assertEquals("12:00 AM. Total distance 20.0 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement); @@ -204,8 +225,11 @@ public void getAnnouncement_withInterval_metric_pace() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, false, lastInterval, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, false, lastInterval, null).toString(); // then assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement); @@ -224,8 +248,11 @@ public void getAnnouncement_imperial_speed() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_FEET, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement); @@ -242,8 +269,11 @@ public void getAnnouncement_imperial_speed_1() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_FEET, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement); @@ -260,8 +290,11 @@ public void getAnnouncement_imperial_meter_speed_1() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_METER, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_METER, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement); @@ -278,8 +311,11 @@ public void getAnnouncement_metric_speed_1() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, null, null).toString(); // then assertEquals("12:00 AM. Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement); @@ -303,8 +339,11 @@ public void getAnnouncement_withInterval_imperial_speed() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, true, lastInterval, null).toString(); // then assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Average moving speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement); @@ -323,8 +362,11 @@ public void getAnnouncement_imperial_pace() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_FEET, false, null, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, null, null).toString(); // then assertEquals("12:00 AM. Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement); @@ -348,8 +390,11 @@ public void getAnnouncement_withInterval_imperial_pace() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.IMPERIAL_FEET, false, lastInterval, null).toString(); // then assertEquals("12:16 AM. Total distance 8.8 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement); @@ -377,17 +422,22 @@ public void getAnnouncement_heart_rate_and_sensor_statistics() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString(); // then - assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement); + assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement); } @Test - public void getAnnouncement_only_lap_heart_rate() { + public void getAnnouncement_heart_rate() { + PreferencesUtils.setVoiceAnnounceHeartRateCurrent(true); PreferencesUtils.setVoiceAnnounceLapHeartRate(true); - PreferencesUtils.setVoiceAnnounceAverageHeartRate(false); + PreferencesUtils.setVoiceAnnounceAverageHeartRate(true); + PreferencesUtils.setVoiceAnnounceTotalDistance(false); PreferencesUtils.setVoiceAnnounceMovingTime(false); PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false); @@ -410,11 +460,14 @@ public void getAnnouncement_only_lap_heart_rate() { Track track = new Track(); track.setTrackStatistics(stats); + SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); + Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); + // when - String announcement = VoiceAnnouncementUtils.createStatistics(context, track, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString(); + String announcement = VoiceAnnouncementUtils.createStatistics(context, track, dataSet, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString(); // then - assertEquals("12:16 AM. Current heart rate 133 bpm.", announcement); + assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement); } @Test diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java index 25f8630ad..e1547b439 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java @@ -20,7 +20,7 @@ import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.settings.PreferencesUtils; -public final class SensorDataSet { +public class SensorDataSet { private static final String TAG = SensorDataSet.class.getSimpleName(); diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index a5b65f228..c25d1edc1 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -317,7 +317,7 @@ private void updateRecordingDataWhileRecording() { // Compute temporary track statistics using sensorData and update time. Pair> data = trackRecordingManager.getDataForUI(); - voiceAnnouncementManager.announceStatisticsIfNeeded(data.first); + voiceAnnouncementManager.announceStatisticsIfNeeded(data.first, data.second.second); recordingDataObservable.postValue(new RecordingData(data.first, data.second.first, data.second.second)); } diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java index fcbc5c368..8cc362a5d 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java @@ -33,6 +33,7 @@ import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; +import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.TrackStatistics; @@ -118,7 +119,7 @@ public void announceIdle() { voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context)); } - public void announceStatisticsIfNeeded(@NonNull Track track) { + public void announceStatisticsIfNeeded(@NonNull Track track, @NonNull SensorDataSet sensorDataSet) { if (shouldNotAnnounce()) { return; } @@ -135,11 +136,11 @@ public void announceStatisticsIfNeeded(@NonNull Track track) { } if (announce) { - voiceAnnouncement.speak(createAnnouncement(track)); + voiceAnnouncement.speak(createAnnouncement(track, sensorDataSet)); } } - private Spannable createAnnouncement(Track track) { + private Spannable createAnnouncement(Track track, SensorDataSet sensorDataSet) { Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance(); if (currentIntervalDistance != intervalDistance) { intervalStatistics = new IntervalStatistics(currentIntervalDistance); @@ -155,7 +156,7 @@ private Spannable createAnnouncement(Track track) { sensorStatistics = contentProviderUtils.getSensorStats(track.getId()); } - return VoiceAnnouncementUtils.createStatistics(context, track, PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics); + return VoiceAnnouncementUtils.createStatistics(context, track, sensorDataSet, PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics); } public void stop() { diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java index 8f6372d4b..ab3be8785 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -3,6 +3,7 @@ import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace; +import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRateCurrent; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapPower; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace; @@ -17,6 +18,7 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.style.TtsSpan; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,8 +31,10 @@ import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.TrackStatistics; @@ -47,7 +51,7 @@ static Spannable createIdle(Context context) { .append(context.getString(R.string.voiceIdle)); } - static Spannable createStatistics(Context context, Track track, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) { + static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) { TrackStatistics trackStatistics = track.getTrackStatistics(); SpannableStringBuilder builder = new SpannableStringBuilder(); @@ -104,92 +108,102 @@ static Spannable createStatistics(Context context, Track track, UnitSystem unitS // Punctuation helps introduce natural pauses in TTS builder.append("."); } - if (totalDistance.isZero()) { - return builder; - } + + boolean hasTravelledDistance = !totalDistance.isZero(); // Announce time Duration movingTime = trackStatistics.getMovingTime(); - if (shouldVoiceAnnounceMovingTime() && !movingTime.isZero()) { + if (shouldVoiceAnnounceMovingTime() && hasTravelledDistance && !movingTime.isZero()) { appendDuration(context, builder, movingTime); builder.append("."); } - if (isReportSpeed) { - if (shouldVoiceAnnounceAverageSpeedPace()) { - double speedInUnit = averageMovingSpeed.to(unitSystem); - builder.append(" ") - .append(context.getString(R.string.speed)); - String template = context.getResources().getString(speedId); - appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS); - builder.append("."); - } - if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) { - double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem); - if (currentDistancePerTimeInUnit > 0) { + if (hasTravelledDistance) { + if (isReportSpeed) { + if (shouldVoiceAnnounceAverageSpeedPace()) { + double speedInUnit = averageMovingSpeed.to(unitSystem); builder.append(" ") - .append(context.getString(R.string.lap_speed)); + .append(context.getString(R.string.speed)); String template = context.getResources().getString(speedId); - appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS); + appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS); builder.append("."); } - } - } else { - if (shouldVoiceAnnounceAverageSpeedPace()) { - Duration time = averageMovingSpeed.toPace(unitSystem); - builder.append(" ") - .append(context.getString(R.string.pace)); - appendDuration(context, builder, time); - builder.append(" ") - .append(context.getString(perUnitStringId)) - .append("."); - } + if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) { + double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem); + if (currentDistancePerTimeInUnit > 0) { + builder.append(" ") + .append(context.getString(R.string.lap_speed)); + String template = context.getResources().getString(speedId); + appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS); + builder.append("."); + } + } + } else { + if (shouldVoiceAnnounceAverageSpeedPace()) { + Duration time = averageMovingSpeed.toPace(unitSystem); + builder.append(" ") + .append(context.getString(R.string.pace)); + appendDuration(context, builder, time); + builder.append(" ") + .append(context.getString(perUnitStringId)) + .append("."); + } - if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) { - Duration currentTime = currentDistancePerTime.toPace(unitSystem); - builder.append(" ") - .append(context.getString(R.string.lap_time)); - appendDuration(context, builder, currentTime); - builder.append(" ") - .append(context.getString(perUnitStringId)) - .append("."); + if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) { + Duration currentTime = currentDistancePerTime.toPace(unitSystem); + builder.append(" ") + .append(context.getString(R.string.lap_time)); + appendDuration(context, builder, currentTime); + builder.append(" ") + .append(context.getString(perUnitStringId)) + .append("."); + } } } - if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) { - int averageHeartRate = Math.round(sensorStatistics.avgHeartRate().getBPM()); + Pair heartrate = sensorDataSet.getHeartRate(); + if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.first != null) { //TODO Check has an announcable value? + int averageHeartRate = Math.round(heartrate.first.getBPM()); builder.append(" ") - .append(context.getString(R.string.average_heart_rate)); + .append(context.getString(R.string.current_heart_rate)); appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate); builder.append("."); } - if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) { - int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM()); + if (hasTravelledDistance) { + if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) { + int averageHeartRate = Math.round(sensorStatistics.avgHeartRate().getBPM()); - builder.append(" ") - .append(context.getString(R.string.current_heart_rate)); - appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate); - builder.append("."); - } + builder.append(" ") + .append(context.getString(R.string.average_heart_rate)); + appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate); + builder.append("."); + } + if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) { + int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM()); - if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) { - int currentPower = Math.round(currentInterval.getAveragePower().getW()); - if(shouldVoiceAnnounceUnit()) { - String template = context.getResources().getString(R.string.power_x_watt); builder.append(" ") - .append(MessageFormat.format(template, Map.of("x", currentPower))); - } else { - builder.append(" ").append(context.getString(R.string.power)).append(" "); - builder.append(String.valueOf(currentPower)); + .append(context.getString(R.string.lap_heart_rate)); + appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate); + builder.append("."); } - builder.append("."); - } + if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) { + int currentPower = Math.round(currentInterval.getAveragePower().getW()); + if (shouldVoiceAnnounceUnit()) { + String template = context.getResources().getString(R.string.power_x_watt); + builder.append(" ") + .append(MessageFormat.format(template, Map.of("x", currentPower))); + } else { + builder.append(" ").append(context.getString(R.string.power)).append(" "); + builder.append(String.valueOf(currentPower)); + } + builder.append("."); + } + } return builder; } - //TODO TtsSpan.TimeBuilder? private static void appendDuration(@NonNull Context context, @NonNull SpannableStringBuilder builder, @NonNull Duration duration) { int hours = (int) (duration.toHours()); int minutes = (int) (duration.toMinutes() % 60); diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 7449050de..38cfbe60a 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -433,14 +433,23 @@ public static void setVoiceAnnounceLapSpeedPace(boolean value) { setBoolean(R.string.voice_announce_lap_speed_pace_key, value); } - public static boolean shouldVoiceAnnounceLapHeartRate() { - return getBoolean(R.string.voice_announce_lap_heart_rate_key, false); - } - public static boolean shouldVoiceAnnounceLapPower() { return getBoolean(R.string.voice_announce_lap_power_key, false); } + public static boolean shouldVoiceAnnounceHeartRateCurrent() { + return getBoolean(R.string.voice_announce_heart_rate_current_key, false); + } + + @VisibleForTesting + public static void setVoiceAnnounceHeartRateCurrent(boolean value) { + setBoolean(R.string.voice_announce_heart_rate_current_key, value); + } + + public static boolean shouldVoiceAnnounceLapHeartRate() { + return getBoolean(R.string.voice_announce_lap_heart_rate_key, false); + } + @VisibleForTesting public static void setVoiceAnnounceLapHeartRate(boolean value) { setBoolean(R.string.voice_announce_lap_heart_rate_key, value); diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index be8d1fa28..4a3ac2651 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -255,6 +255,8 @@ voiceAnnounceUnit true + voiceAnnounceHeartRateCurrent + false voiceAnnounceAverageHeartRate false voiceAnnounceLapHeartRate diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9a37cf9a6..f9de7b82f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -609,6 +609,7 @@ limitations under the License. Average heart rate Current heart rate + Lap heart rate ATM bank diff --git a/src/main/res/xml/settings_announcements.xml b/src/main/res/xml/settings_announcements.xml index eee1cf8ab..3adf5fa29 100644 --- a/src/main/res/xml/settings_announcements.xml +++ b/src/main/res/xml/settings_announcements.xml @@ -59,6 +59,11 @@ android:key="@string/voice_announce_average_speed_pace_key" android:title="@string/settings_announcements_average_speed_pace" /> + +