diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java index 007c80d28..373a1bd23 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java @@ -69,7 +69,9 @@ public class VoiceAnnouncement { public void onAudioFocusChange(int focusChange) { Log.d(TAG, "Audio focus changed to " + focusChange); - boolean stop = List.of(AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) + boolean stop = List + .of(AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) .contains(focusChange); if (stop && tts != null && tts.isSpeaking()) { @@ -82,7 +84,8 @@ public void onAudioFocusChange(int focusChange) { private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() { @Override public void onStart(String utteranceId) { - int result = audioManager.requestAudioFocus(audioFocusChangeListener, AUDIO_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + int result = audioManager.requestAudioFocus(audioFocusChangeListener, AUDIO_STREAM, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { Log.w(TAG, "Failed to request audio focus."); } @@ -142,7 +145,7 @@ public void start() { } } - public void announceatTheEndOfTrack(){ + public void announceatTheEndOfTrack() { synchronized (this) { if (!ttsReady) { ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; @@ -172,7 +175,8 @@ public void announceatTheEndOfTrack(){ Spannable announcement = VoiceAnnouncementUtils.atTheEndAnnounce(); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); } - public void announceMotivation(){ + + public void announceMotivation() { synchronized (this) { if (!ttsReady) { ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; @@ -202,7 +206,8 @@ public void announceMotivation(){ Spannable announcement = VoiceAnnouncementUtils.getMotivationalAnnouncements(); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); } - public void announceMotivationForIncreasedSpeed(){ + + public void announceMotivationForIncreasedSpeed() { synchronized (this) { if (!ttsReady) { ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; @@ -233,7 +238,7 @@ public void announceMotivationForIncreasedSpeed(){ tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); } - public void announce(@NonNull Track track) { + public void announceMotivationForDecreasedSpeed() { synchronized (this) { if (!ttsReady) { ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; @@ -243,8 +248,6 @@ public void announce(@NonNull Track track) { } } - - if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION) .contains(audioManager.getMode())) { Log.i(TAG, "Announcement is not allowed at this time."); @@ -262,7 +265,36 @@ public void announce(@NonNull Track track) { return; } + Spannable announcement = VoiceAnnouncementUtils.getMotivationalAnnouncementsForSpeedDecreased(); + tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); + } + + public void announce(@NonNull Track track) { + synchronized (this) { + if (!ttsReady) { + ttsReady = ttsInitStatus == TextToSpeech.SUCCESS; + if (ttsReady) { + onTtsReady(); + } + } + } + if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION) + .contains(audioManager.getMode())) { + Log.i(TAG, "Announcement is not allowed at this time."); + return; + } + + if (!ttsReady) { + if (ttsFallback == null) { + Log.w(TAG, "MediaPlayer for ttsFallback was not created."); + } else { + Log.i(TAG, "TTS not ready/available, just generating a tone."); + ttsFallback.seekTo(0); + ttsFallback.start(); + } + return; + } Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance(); if (currentIntervalDistance != intervalDistance) { @@ -271,7 +303,8 @@ public void announce(@NonNull Track track) { startTrackPointId = null; } - TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId); + TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), + startTrackPointId); startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); SensorStatistics sensorStatistics = null; @@ -279,13 +312,16 @@ public void announce(@NonNull Track track) { sensorStatistics = contentProviderUtils.getSensorStats(track.getId()); } -// Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics); - // SpannableStringBuilder announcement = new SpannableStringBuilder(); + // Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, + // track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), + // PreferencesUtils.isReportSpeed(track), lastInterval, sensorStatistics); + // SpannableStringBuilder announcement = new SpannableStringBuilder(); Spannable announcement = VoiceAnnouncementUtils.getMotivationalAnnouncements(); - // announcement.append("good job"); + // announcement.append("good job"); if (announcement.length() > 0) { - // We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called. + // 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"); } } @@ -305,12 +341,14 @@ public void stop() { private void onTtsReady() { Locale locale = Locale.getDefault(); int languageAvailability = tts.isLanguageAvailable(locale); - if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { + if (languageAvailability == TextToSpeech.LANG_MISSING_DATA + || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { Log.w(TAG, "Default locale not available, use English."); locale = Locale.ENGLISH; /* - * TODO: instead of using english, load the language if missing and show a toast if not supported. - * Not able to change the resource strings to English. + * TODO: instead of using english, load the language if missing and show a toast + * if not supported. + * Not able to change the resource strings to English. */ } tts.setLanguage(locale); 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 afdf1d1dc..c495deb7e 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java @@ -62,7 +62,6 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe @NonNull private Duration nextTotalTime = TOTALTIME_OFF; - public VoiceAnnouncementManager(@NonNull TrackRecordingService trackRecordingService) { this.trackRecordingService = trackRecordingService; } @@ -78,13 +77,20 @@ void update(@Nullable TrackStatistics trackStatistics) { updateNextDuration(); updateNextTaskDistance(); } + boolean isSignificantSpeedIncreased(Speed max, Speed avg) { - // Define your criteria for a significant speed change (e.g., double or reduce by a certain percentage) + // Define your criteria for a significant speed change (e.g., double or reduce + // by a certain percentage) double speedChangeThreshold = 0.5; // Example: 50% return Math.abs(max.toMPH() - avg.toMPH()) / max.toMPH() >= speedChangeThreshold; } + boolean isSignificantSpeedDecreased(Speed max, Speed avg) { + double speedChangeThreshold = 0.4; + return Math.abs(max.toMPH() - avg.toMPH()) / max.toMPH() <= speedChangeThreshold; + } + public void update(@NonNull Context context, @NonNull Track track) { if (voiceAnnouncement == null) { Log.e(TAG, "Cannot update when in status shutdown."); @@ -93,53 +99,54 @@ public void update(@NonNull Context context, @NonNull Track track) { if (!PreferencesUtils.shouldVoiceAnnouncementOnDeviceSpeaker() && MediaRouter.getInstance(context) - .getSelectedRoute() - .isDeviceSpeaker()) { + .getSelectedRoute() + .isDeviceSpeaker()) { Log.i(TAG, "No voice announcement on device speaker."); return; } - - boolean announce = false; - boolean announceForSpeed =false; - boolean finalannounce=false; + boolean announceForSpeed = false; + boolean finalannounce = false; + boolean finalSpeedDecreaseAnnounce = false; this.trackStatistics = track.getTrackStatistics(); if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) { updateNextTaskDistance(); announce = true; - announceForSpeed=true; + announceForSpeed = true; } if (!trackStatistics.getTotalTime().minus(nextTotalTime).isNegative() && announceForSpeed) { updateNextDuration(); announce = true; - announceForSpeed=true; + announceForSpeed = true; } Speed maxSpeed = track.getTrackStatistics().getMaxSpeed(); - Speed avgSpeed =track.getTrackStatistics().getAverageSpeed(); - Speed current=Speed.of(trackStatistics.getTotalDistance(),trackStatistics.getTotalTime()); + Speed avgSpeed = track.getTrackStatistics().getAverageSpeed(); + Speed current = Speed.of(trackStatistics.getTotalDistance(), trackStatistics.getTotalTime()); if (isSignificantSpeedIncreased(maxSpeed, avgSpeed) && announceForSpeed) { // Make a motivational announcement finalannounce = true; } - if (finalannounce) { - - voiceAnnouncement.announceMotivationForIncreasedSpeed(); - } - - - + if (isSignificantSpeedDecreased(maxSpeed, avgSpeed) && announceForSpeed) { + finalSpeedDecreaseAnnounce = true; + } + if (finalannounce) { + voiceAnnouncement.announceMotivationForIncreasedSpeed(); + } + if (finalSpeedDecreaseAnnounce) { + voiceAnnouncement.announceMotivationForDecreasedSpeed(); + } if (announce) { Random random = new Random(); float p = random.nextFloat(); - if(p < 0.7){ + if (p < 0.7) { voiceAnnouncement.announce(track); } else { voiceAnnouncement.announceMotivation(); @@ -147,9 +154,6 @@ public void update(@NonNull Context context, @NonNull Track track) { } } - - - public void stop() { voiceAnnouncement.announceatTheEndOfTrack(); if (voiceAnnouncement != null) { @@ -158,8 +162,6 @@ public void stop() { } } - - public void setFrequency(Duration frequency) { this.totalTimeFrequency = frequency; update(this.trackStatistics); @@ -189,7 +191,8 @@ private void updateNextDuration() { } else { Duration totalTime = trackStatistics.getTotalTime(); - Duration intervalMod = Duration.ofMillis(trackStatistics.getTotalTime().toMillis() % totalTimeFrequency.toMillis()); + Duration intervalMod = Duration + .ofMillis(trackStatistics.getTotalTime().toMillis() % totalTimeFrequency.toMillis()); nextTotalTime = totalTime.plus(totalTimeFrequency.minus(intervalMod)); } @@ -213,7 +216,8 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin setFrequency(PreferencesUtils.getVoiceAnnouncementFrequency()); } - if (PreferencesUtils.isKey(new int[]{R.string.voice_announcement_distance_key, R.string.stats_units_key}, key)) { + if (PreferencesUtils.isKey(new int[] { R.string.voice_announcement_distance_key, R.string.stats_units_key }, + key)) { setFrequency(PreferencesUtils.getVoiceAnnouncementDistance()); } } 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 5fbd27c28..8c06904fd 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -1,6 +1,6 @@ package de.dennisguse.opentracks.services.announcement; -import java.util.Random; +import java.util.Random; import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace; @@ -10,7 +10,6 @@ import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTime; - import android.content.Context; import android.icu.text.MessageFormat; import android.text.Spannable; @@ -39,18 +38,21 @@ class VoiceAnnouncementUtils { private VoiceAnnouncementUtils() { } - static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) { + static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, + boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, + @Nullable SensorStatistics sensorStatistics) { SpannableStringBuilder builder = new SpannableStringBuilder(); Distance totalDistance = trackStatistics.getTotalDistance(); Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed(); Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null; // Announce current time - if(shouldVoiceAnnounceTime()){ + if (shouldVoiceAnnounceTime()) { SpeechTxtForTime t = new SpeechTxtForTime(); CTime c = new CTime(); String currentTime = c.getCurrentTime(); - builder.append(t.speechText+currentTime+".");} + builder.append(t.speechText + currentTime + "."); + } int perUnitStringId; int distanceId; @@ -87,16 +89,17 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic if (shouldVoiceAnnounceTotalDistance()) { 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 + // See + // https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE String template = context.getResources().getString(distanceId); - appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", distanceInUnit)), distanceInUnit, 1, unitDistanceTTS); + appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", distanceInUnit)), distanceInUnit, 1, + unitDistanceTTS); // Punctuation helps introduce natural pauses in TTS builder.append("."); } if (totalDistance.isZero()) { return builder; } - // Announce time Duration movingTime = trackStatistics.getMovingTime(); @@ -105,14 +108,14 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic 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); + appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, + unitSpeedTTS); builder.append("."); } if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) { @@ -121,7 +124,9 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic 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); + appendDecimalUnit(builder, + MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), + currentDistancePerTimeInUnit, 1, unitSpeedTTS); builder.append("."); } } @@ -152,7 +157,8 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic builder.append(" ") .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("."); } if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) { @@ -160,36 +166,46 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic builder.append(" ") .append(context.getString(R.string.current_heart_rate)); - appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), currentHeartRate); + appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, currentHeartRate), + currentHeartRate); builder.append("."); } return builder; } - static Spannable atTheEndAnnounce(){ - String ma="Your session as ended.Remember! Consistency is the key to success"; + static Spannable atTheEndAnnounce() { + String ma = "Your session as ended.Remember! Consistency is the key to success"; SpannableStringBuilder motivator = new SpannableStringBuilder(); return (motivator.append(ma)); } - - static Spannable getMotivationalAnnouncements(){ + static Spannable getMotivationalAnnouncements() { MotivationalAnnouncements ma = new MotivationalAnnouncements(); Random random = new Random(); - int i = random.nextInt(ma.motivations.length); //random number between 0 (inclusive) and length of the array (exclusive) + int i = random.nextInt(ma.motivations.length); // random number between 0 (inclusive) and length of the array + // (exclusive) SpannableStringBuilder motivator = new SpannableStringBuilder(); return (motivator.append(ma.motivations[i])); } - static Spannable getMotivationalAnnouncementsForSpeedIncreased(){ + + static Spannable getMotivationalAnnouncementsForSpeedIncreased() { MotivationalAnnouncements ma = new MotivationalAnnouncements(); SpannableStringBuilder motivator = new SpannableStringBuilder(); return (motivator.append(ma.speedIncreased_motivations)); } - private static void appendDuration(@NonNull Context context, @NonNull SpannableStringBuilder builder, @NonNull Duration duration) { + static Spannable getMotivationalAnnouncementsForSpeedDecreased() { + MotivationalAnnouncements ma = new MotivationalAnnouncements(); + + SpannableStringBuilder motivator = new SpannableStringBuilder(); + return (motivator.append(ma.speedDecreasedMotivation)); + } + + 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); @@ -209,17 +225,19 @@ private static void appendDuration(@NonNull Context context, @NonNull SpannableS } /** - * Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct plural form] + * Speaks as: 98.14 [UNIT] - ninety eight point one four [UNIT with correct + * plural form] * - * @param number The number to speak + * @param number The number to speak * @param precision The number of decimal places to announce */ - private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, double number, int precision, @NonNull String unit) { + private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, + double number, int precision, @NonNull String unit) { TtsSpan.MeasureBuilder measureBuilder = new TtsSpan.MeasureBuilder() .setUnit(unit); if (precision == 0) { - measureBuilder.setNumber((long)number); + measureBuilder.setNumber((long) number); } else { // Round before extracting integral and decimal parts double roundedNumber = Math.round(Math.pow(10, precision) * number) / Math.pow(10.0, precision); @@ -237,9 +255,10 @@ private static void appendDecimalUnit(@NonNull SpannableStringBuilder builder, @ /** * Speaks as: 98 - ninety eight */ - private static void appendCardinal(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, long number) { + private static void appendCardinal(@NonNull SpannableStringBuilder builder, @NonNull String localizedText, + long number) { builder.append(" ") - .append(localizedText, new TtsSpan.CardinalBuilder().setNumber(number).build(), SPAN_INCLUSIVE_EXCLUSIVE); + .append(localizedText, new TtsSpan.CardinalBuilder().setNumber(number).build(), + SPAN_INCLUSIVE_EXCLUSIVE); } } - diff --git a/src/main/java/de/dennisguse/opentracks/util/MotivationalAnnouncements.java b/src/main/java/de/dennisguse/opentracks/util/MotivationalAnnouncements.java index 7ef551111..dc2d6d818 100644 --- a/src/main/java/de/dennisguse/opentracks/util/MotivationalAnnouncements.java +++ b/src/main/java/de/dennisguse/opentracks/util/MotivationalAnnouncements.java @@ -1,10 +1,10 @@ package de.dennisguse.opentracks.util; public class MotivationalAnnouncements { - public String []motivations = {"You are doing great","Kudos","Go Hard or Go Home","Yippee","Faster if you can","Its okay,You can do this","I believe in you", - "You're the best sportsperson I have ever seen","Keep going"}; + public String[] motivations = { "You are doing great", "Kudos", "Go Hard or Go Home", "Yippee", "Faster if you can", + "Its okay,You can do this", "I believe in you", + "You're the best sportsperson I have ever seen", "Keep going" }; public String speedIncreased_motivations = "Hurray you have doubled up your speed"; - - + public String speedDecreasedMotivation = "You can do better you can increase your speed"; }