diff --git a/build.gradle b/build.gradle
index 9e28c621d..fc9e05552 100644
--- a/build.gradle
+++ b/build.gradle
@@ -69,7 +69,7 @@ android {
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
- minSdk 21
+ minSdk 24
targetSdk 34
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
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 aea280e68..079b978e6 100644
--- a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java
+++ b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.services.announcement;
import static org.junit.Assert.assertEquals;
import android.content.Context;
+import android.icu.text.MessageFormat;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
@@ -15,6 +16,7 @@ import org.junit.runner.RunWith;
import java.time.Duration;
import java.util.Locale;
+import java.util.Map;
import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -191,6 +193,34 @@ public class VoiceAnnouncementUtilsTest {
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
}
+ @Test
+ public void getAnnouncement_imperial_speed_1() {
+ TrackStatistics stats = new TrackStatistics();
+ stats.setTotalDistance(Distance.ofMile(1.1));
+ stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
+ stats.setMovingTime(Duration.ofHours(1));
+
+ // when
+ String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();
+
+ // then
+ assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
+ }
+
+ @Test
+ public void getAnnouncement_metric_speed_1() {
+ TrackStatistics stats = new TrackStatistics();
+ stats.setTotalDistance(Distance.ofKilometer(1.1));
+ stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
+ stats.setMovingTime(Duration.ofHours(1));
+
+ // when
+ String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
+
+ // then
+ assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
+ }
+
@Test
public void getAnnouncement_withInterval_imperial_speed() {
// given
@@ -306,4 +336,19 @@ public class VoiceAnnouncementUtilsTest {
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
}
+
+ @Test
+ public void ICUMessageDemo() {
+ // Android 7's ICU MessageFormat; working
+ String template = """
+ {n, plural,
+ one {1 mile}
+ other {{n,number,#.#} miles}
+ }""";
+
+ assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.1)));
+ assertEquals("1 mile", MessageFormat.format(template, Map.of("n", 1)));
+ assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.11)));
+ assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18)));
+ }
}
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 f28518fac..b9d5d6124 100644
--- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java
@@ -9,6 +9,7 @@ import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnno
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import android.content.Context;
+import android.icu.text.MessageFormat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.TtsSpan;
@@ -17,6 +18,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
+import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
@@ -45,22 +47,22 @@ class VoiceAnnouncementUtils {
switch (unitSystem) {
case METRIC -> {
perUnitStringId = R.string.voice_per_kilometer;
- distanceId = R.plurals.voiceDistanceKilometers;
- speedId = R.plurals.voiceSpeedKilometersPerHour;
+ distanceId = R.string.voiceDistanceKilometersPlural;
+ speedId = R.string.voiceSpeedKilometersPerHourPlural;
unitDistanceTTS = "kilometer";
unitSpeedTTS = "kilometer per hour";
}
case IMPERIAL_FEET -> {
perUnitStringId = R.string.voice_per_mile;
- distanceId = R.plurals.voiceDistanceMiles;
- speedId = R.plurals.voiceSpeedMilesPerHour;
+ distanceId = R.string.voiceDistanceMilesPlural;
+ speedId = R.string.voiceSpeedMilesPerHourPlural;
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
}
case NAUTICAL_IMPERIAL -> {
perUnitStringId = R.string.voice_per_nautical_mile;
- distanceId = R.plurals.voiceDistanceNauticalMiles;
- speedId = R.plurals.voiceSpeedMKnots;
+ distanceId = R.string.voiceDistanceNauticalMilesPlural;
+ speedId = R.string.voiceSpeedMKnotsPlural;
unitDistanceTTS = "nautical mile";
unitSpeedTTS = "knots";
}
@@ -73,7 +75,8 @@ class VoiceAnnouncementUtils {
builder.append(context.getString(R.string.total_distance));
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
- appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 1, unitDistanceTTS);
+ String template = context.getResources().getString(distanceId);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", distanceInUnit)), distanceInUnit, 1, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
@@ -93,7 +96,8 @@ class VoiceAnnouncementUtils {
double speedInUnit = averageMovingSpeed.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
- appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
+ String template = context.getResources().getString(speedId);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
@@ -101,7 +105,8 @@ class VoiceAnnouncementUtils {
if (currentDistancePerTimeInUnit > 0) {
builder.append(" ")
.append(context.getString(R.string.lap_speed));
- appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
+ String template = context.getResources().getString(speedId);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
@@ -147,23 +152,22 @@ class VoiceAnnouncementUtils {
return builder;
}
- static int getQuantityCount(double d) {
- return (int) d;
- }
-
private static void appendDuration(@NonNull Context context, @NonNull SpannableStringBuilder builder, @NonNull Duration duration) {
int hours = (int) (duration.toHours());
int minutes = (int) (duration.toMinutes() % 60);
int seconds = (int) (duration.getSeconds() % 60);
if (hours > 0) {
- appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
+ String template = context.getResources().getString(R.string.voiceHoursPlural);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", hours)), hours, 0, "hour");
}
if (minutes > 0) {
- appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
+ String template = context.getResources().getString(R.string.voiceMinutesPlural);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", minutes)), minutes, 0, "minute");
}
if (seconds > 0 || duration.isZero()) {
- appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
+ String template = context.getResources().getString(R.string.voiceSecondsPlural);
+ appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", seconds)), seconds, 0, "second");
}
}
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 872996f9f..8495cda25 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -554,29 +554,65 @@ limitations under the License.
Smallest (recommended)
- - 1 hour
- - %1$d hours
+ - 1 hour [Replaced by VoiceHoursPlural]
+ - %1$d hours [Replaced by VoiceHoursPlural]
- - 1 minute
- - %1$d minutes
+ - 1 minute [Replaced by VoiceMinutesPlural]
+ - %1$d minutes [Replaced by VoiceMinutesPlural]
- - 1 second
- - %1$d seconds
+ - 1 second [Replaced by voiceSecondsPlural]
+ - %1$d seconds [Replaced by voiceSecondsPlural]
- - 1 kilometer per hour
- - %1$.1f kilometers per hour
+ - 1 kilometer per hour [Replaced by voiceSpeedKilometersPerHourPlural]
+ - %1$.1f kilometers per hour [Replaced by voiceSpeedKilometersPerHourPlural]
- - 1 mile per hour
- - %1$.1f miles per hour
+ - 1 mile per hour [Replaced by voiceSpeedMilesPerHourPlural]
+ - %1$.1f miles per hour [Replaced by voiceSpeedMilesPerHourPlural]
- - 1 knot
- - %1$.1f knots
+ - 1 knot [Replaced by voiceSpeedMKnotsPlural]
+ - %1$.1f knots [Replaced by voiceSpeedMKnotsPlural]
+
+ {n, plural,
+ =1 {1 hour}
+ other {{n,number} hours}
+ }
+
+
+ {n, plural,
+ =1 {1 minute}
+ other {{n,number} minutes}
+ }
+
+
+ {n, plural,
+ =1 {1 second}
+ other {{n,number} seconds}
+ }
+
+
+ {n, plural,
+ =1 {1 kilometer per hour}
+ other {{n,number,#.0} kilometers per hour}
+ }
+
+
+ {n, plural,
+ =1 {1 mile per hour}
+ other {{n,number,#.0} miles per hour}
+ }
+
+
+ {n, plural,
+ =1 {1 knot}
+ other {{n,number,#.0} knots}
+ }
+
Use the device\'s speaker
per kilometer
per mile
@@ -587,17 +623,35 @@ limitations under the License.
Average moving speed
Total distance
- - 1 kilometer
- - %1$.1f kilometers
+ - 1 kilometer [Replaced by voiceDistanceKilometersPlural]
+ - %1$.1f kilometers [Replaced by voiceDistanceKilometersPlural]
- - 1 mile
- - %1$.1f miles
+ - 1 mile [Replaced by voiceDistanceMilesPlural]
+ - %1$.1f miles [Replaced by voiceDistanceMilesPlural]
- - 1 nautical mile
- - %1$.1f nautical miles
+ - 1 nautical mile [Replaced by voiceDistanceNauticalMilesPlural]
+ - %1$.1f nautical miles [Replaced by voiceDistanceNauticalMilesPlural]
+
+ {n, plural,
+ =1 {1 kilometer}
+ other {{n,number,#.0} kilometers}
+ }
+
+
+ {n, plural,
+ =1 {1 mile}
+ other {{n,number,#.0} miles}
+ }
+
+
+ {n, plural,
+ =1 {1 nautical mile}
+ other {{n,number,#.0} nautical miles}
+ }
+
Average heart rate
Current heart rate