Voice announcements: added settings for what to announce

This commit is contained in:
Jonas Kalderstam
2022-02-20 14:42:46 +01:00
committed by Dennis Guse
parent 14b705b43e
commit 00351136f2
7 changed files with 187 additions and 59 deletions
@@ -42,7 +42,13 @@ public class VoiceAnnouncementUtilsTest {
@Before @Before
public void setUp() { public void setUp() {
contentProviderUtils = new ContentProviderUtils(context); contentProviderUtils = new ContentProviderUtils(context);
PreferencesUtils.setVoiceAnnounceHeartRate(false);
PreferencesUtils.setVoiceAnnounceLapHeartRate(false);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceTotalDistance(true);
PreferencesUtils.setVoiceAnnounceTotalTime(true);
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(true);
PreferencesUtils.setVoiceAnnounceLapSpeedPace(true);
} }
@Test @Test
@@ -199,7 +205,8 @@ public class VoiceAnnouncementUtilsTest {
@Test @Test
public void getAnnouncement_heart_rate_and_sensor_statistics() { public void getAnnouncement_heart_rate_and_sensor_statistics() {
PreferencesUtils.setVoiceAnnounceHeartRate(true); PreferencesUtils.setVoiceAnnounceAverageHeartRate(true);
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
int numberOfPoints = 1000; int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
@@ -221,4 +228,34 @@ public class VoiceAnnouncementUtilsTest {
// then // then
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. 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("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
} }
@Test
public void getAnnouncement_only_lap_heart_rate() {
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
PreferencesUtils.setVoiceAnnounceAverageHeartRate(false);
PreferencesUtils.setVoiceAnnounceTotalDistance(false);
PreferencesUtils.setVoiceAnnounceTotalTime(false);
PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false);
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
int numberOfPoints = 1000;
Pair<Track.Id, TrackStatistics> trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints);
Track.Id trackId = trackWithStats.first;
TrackStatistics stats = trackWithStats.second;
IntervalStatistics.Interval lastInterval;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) {
assertEquals(trackPointIterator.getCount(), numberOfPoints);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000));
intervalStatistics.addTrackPoints(trackPointIterator);
lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
}
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, sensorStatistics).toString();
// then
assertEquals(" Current heart rate 132 bpm,", announcement);
}
} }
@@ -181,8 +181,10 @@ public class VoiceAnnouncement {
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics); Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called. if (announcement.length() > 0) {
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); // We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
}
} }
public void stop() { public void stop() {
@@ -1,7 +1,12 @@
package de.dennisguse.opentracks.services.announcement; package de.dennisguse.opentracks.services.announcement;
import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE; import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRate; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalTime;
import android.content.Context; import android.content.Context;
import android.text.Spannable; import android.text.Spannable;
@@ -58,38 +63,42 @@ class VoiceAnnouncementUtils {
double distanceInUnit = distance.toKM_Miles(unitSystem); double distanceInUnit = distance.toKM_Miles(unitSystem);
builder.append(context.getString(R.string.total_distance)); if (shouldVoiceAnnounceTotalDistance()) {
long distanceIntegerPart = (long) distanceInUnit; builder.append(context.getString(R.string.total_distance));
// Extract the decimal part long distanceIntegerPart = (long) distanceInUnit;
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2); // Extract the decimal part
// Units should always be english singular for TTS. String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE // Units should always be english singular for TTS.
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS); // See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
// Punctuation helps introduce natural pauses in TTS appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
builder.append("."); // Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
if (distance.isZero()) { if (distance.isZero()) {
return builder; return builder;
} }
// Announce time // Announce time
Duration movingTime = trackStatistics.getMovingTime(); Duration movingTime = trackStatistics.getMovingTime();
if (!movingTime.isZero()) { if (shouldVoiceAnnounceTotalTime() && !movingTime.isZero()) {
appendDuration(context, builder, movingTime); appendDuration(context, builder, movingTime);
builder.append("."); builder.append(".");
} }
if (isReportSpeed) { if (isReportSpeed) {
double speedInUnit = distancePerTime.to(unitSystem); if (shouldVoiceAnnounceAverageSpeedPace()) {
double speedInUnit = distancePerTime.to(unitSystem);
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.speed)); .append(context.getString(R.string.speed));
long speedIntegerPart = (long) speedInUnit; long speedIntegerPart = (long) speedInUnit;
// Extract the decimal part // Extract the decimal part
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2); String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS); appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
builder.append("."); builder.append(".");
}
if (currentDistancePerTime != null) { if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem); double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) { if (currentDistancePerTimeInUnit > 0) {
@@ -104,16 +113,18 @@ class VoiceAnnouncementUtils {
} }
} }
} else { } else {
Duration time = distancePerTime.toPace(unitSystem); if (shouldVoiceAnnounceAverageSpeedPace()) {
builder.append(" ") Duration time = distancePerTime.toPace(unitSystem);
.append(context.getString(R.string.pace)); builder.append(" ")
appendDuration(context, builder, time); .append(context.getString(R.string.pace));
builder.append(" ") appendDuration(context, builder, time);
.append(context.getString(perUnitStringId)) builder.append(" ")
.append("."); .append(context.getString(perUnitStringId))
.append(".");
}
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0); if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
if (!currentTime.isZero()) { Duration currentTime = currentDistancePerTime.toPace(unitSystem);
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.lap_time)); .append(context.getString(R.string.lap_time));
appendDuration(context, builder, currentTime); appendDuration(context, builder, currentTime);
@@ -123,24 +134,21 @@ class VoiceAnnouncementUtils {
} }
} }
if (shouldVoiceAnnounceHeartRate()) { if (shouldVoiceAnnounceAverageHeartRate() && sensorStatistics != null && sensorStatistics.hasHeartRate()) {
if (sensorStatistics != null && sensorStatistics.hasHeartRate()) { int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
int averageHeartRate = Math.round(sensorStatistics.getAvgHeartRate().getBPM());
builder.append(" ") builder.append(" ")
.append(context.getString(R.string.average_heart_rate)); .append(context.getString(R.string.average_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate); appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate);
builder.append("."); builder.append(".");
} }
if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) {
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM());
if (currentInterval != null && currentInterval.hasAverageHeartRate()) { builder.append(" ")
int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM()); .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.current_heart_rate));
appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate);
builder.append(".");
}
} }
return builder; return builder;
@@ -343,13 +343,58 @@ public class PreferencesUtils {
return getFloat(R.string.voice_speed_rate_key, DEFAULT); return getFloat(R.string.voice_speed_rate_key, DEFAULT);
} }
public static boolean shouldVoiceAnnounceHeartRate() { public static boolean shouldVoiceAnnounceTotalDistance() {
return getBoolean(R.string.voice_announce_heart_rate_key, false); return getBoolean(R.string.voice_announce_total_distance_key, true);
} }
@VisibleForTesting @VisibleForTesting
public static void setVoiceAnnounceHeartRate(boolean value) { public static void setVoiceAnnounceTotalDistance(boolean value) {
setBoolean(R.string.voice_announce_heart_rate_key, value); setBoolean(R.string.voice_announce_total_distance_key, value);
}
public static boolean shouldVoiceAnnounceTotalTime() {
return getBoolean(R.string.voice_announce_total_time_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceTotalTime(boolean value) {
setBoolean(R.string.voice_announce_total_time_key, value);
}
public static boolean shouldVoiceAnnounceAverageSpeedPace() {
return getBoolean(R.string.voice_announce_average_speed_pace_key, true);
}
@VisibleForTesting
public static void setVoiceAnnounceAverageSpeedPace(boolean value) {
setBoolean(R.string.voice_announce_average_speed_pace_key, value);
}
public static boolean shouldVoiceAnnounceLapSpeedPace() {
return getBoolean(R.string.voice_announce_lap_speed_pace_key, true);
}
@VisibleForTesting
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);
}
@VisibleForTesting
public static void setVoiceAnnounceLapHeartRate(boolean value) {
setBoolean(R.string.voice_announce_lap_heart_rate_key, value);
}
public static boolean shouldVoiceAnnounceAverageHeartRate() {
return getBoolean(R.string.voice_announce_average_heart_rate_key, false);
}
@VisibleForTesting
public static void setVoiceAnnounceAverageHeartRate(boolean value) {
setBoolean(R.string.voice_announce_average_heart_rate_key, value);
} }
public static Distance getRecordingDistanceInterval() { public static Distance getRecordingDistanceInterval() {
+6 -1
View File
@@ -291,7 +291,12 @@
<item>1.2</item> <item>1.2</item>
<item>1.3</item> <item>1.3</item>
</string-array> </string-array>
<string name="voice_announce_heart_rate_key" translatable="false">voiceAnnounceHeartRate</string> <string name="voice_announce_total_distance_key" translatable="false">voiceAnnounceTotalDistance</string>
<string name="voice_announce_total_time_key" translatable="false">voiceAnnounceTotalTime</string>
<string name="voice_announce_average_speed_pace_key" translatable="false">voiceAnnounceAverageSpeedPace</string>
<string name="voice_announce_lap_speed_pace_key" translatable="false">voiceAnnounceLapSpeedPace</string>
<string name="voice_announce_average_heart_rate_key" translatable="false">voiceAnnounceAverageHeartRate</string>
<string name="voice_announce_lap_heart_rate_key" translatable="false">voiceAnnounceLapHeartRate</string>
<string name="export_trackfileformat_key" translatable="false">exportTrackFileFormat</string> <string name="export_trackfileformat_key" translatable="false">exportTrackFileFormat</string>
<!-- See TrackFileFormat --> <!-- See TrackFileFormat -->
+4 -1
View File
@@ -395,7 +395,10 @@ limitations under the License.
<string name="settings_announcements_totaltime_frequency">Time interval</string> <string name="settings_announcements_totaltime_frequency">Time interval</string>
<string name="settings_announcements_distance_frequency">Distance interval</string> <string name="settings_announcements_distance_frequency">Distance interval</string>
<string name="settings_announcements_report_heart_rate">Report heart rate</string> <string name="settings_announcements_average_heart_rate">Average heart rate</string>
<string name="settings_announcements_lap_heart_rate">Lap heart rate</string>
<string name="settings_announcements_average_speed_pace">Average speed/pace</string>
<string name="settings_announcements_lap_speed_pace">Lap speed/pace</string>
<!-- Custom Layout --> <!-- Custom Layout -->
<string name="custom_layout_list_title">Layouts</string> <string name="custom_layout_list_title">Layouts</string>
+32 -4
View File
@@ -26,9 +26,37 @@
android:title="@string/menu_voice_rate" android:title="@string/menu_voice_rate"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<SwitchPreference <PreferenceCategory android:title="@string/settings_announcements">
android:defaultValue="false"
android:key="@string/voice_announce_heart_rate_key" <SwitchPreference
android:title="@string/settings_announcements_report_heart_rate" /> android:defaultValue="true"
android:key="@string/voice_announce_total_distance_key"
android:title="@string/total_distance" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/voice_announce_total_time_key"
android:title="@string/stats_total_time" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/voice_announce_average_speed_pace_key"
android:title="@string/settings_announcements_average_speed_pace" />
<SwitchPreference
android:defaultValue="true"
android:key="@string/voice_announce_lap_speed_pace_key"
android:title="@string/settings_announcements_lap_speed_pace" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/voice_announce_average_heart_rate_key"
android:title="@string/settings_announcements_average_heart_rate" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/voice_announce_lap_heart_rate_key"
android:title="@string/settings_announcements_lap_heart_rate" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>