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