diff --git a/src/main/java/de/dennisguse/opentracks/AbstractActivity.java b/src/main/java/de/dennisguse/opentracks/AbstractActivity.java index 33ec9310f..1f264bd79 100644 --- a/src/main/java/de/dennisguse/opentracks/AbstractActivity.java +++ b/src/main/java/de/dennisguse/opentracks/AbstractActivity.java @@ -22,8 +22,7 @@ import android.view.View; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; - -import de.dennisguse.opentracks.util.TTSUtils; +import de.dennisguse.opentracks.services.tasks.VoiceAnnouncement; /** * @author Jimmy Shih @@ -35,7 +34,7 @@ public abstract class AbstractActivity extends AppCompatActivity { super.onCreate(savedInstanceState); // Set volume control stream for text to speech - setVolumeControlStream(TTSUtils.getTTSStream()); + setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM); setContentView(getRootView()); diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index cdefbef8b..8d93da21b 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -431,7 +431,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator. // Compute temporary track statistics using sensorData and update time. TrackPointCreator localHandlerServer = this.handlerServer; - if (localHandlerServer == null) { + VoiceAnnouncementManager localVoiceAnnouncementManager = this.voiceAnnouncementManager; + if (localHandlerServer == null || localVoiceAnnouncementManager == null) { // when this happens, no recording is running and we should not send any notifications. //TODO This implementation is not a good idea; rather solve the issue for this properly return; @@ -445,7 +446,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator. TrackPoint trackPoint = data.second.first; egm2008CorrectionManager.correctAltitude(this, trackPoint); - voiceAnnouncementManager.update(data.first); + localVoiceAnnouncementManager.update(data.first); recordingDataObservable.postValue(new RecordingData(data.first, trackPoint, data.second.second)); } diff --git a/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncement.java b/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncement.java index c1aee351d..01d9130b8 100644 --- a/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncement.java +++ b/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncement.java @@ -19,6 +19,7 @@ package de.dennisguse.opentracks.services.tasks; import android.content.Context; import android.content.SharedPreferences; import android.media.AudioManager; +import android.media.MediaPlayer; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.util.Log; @@ -28,6 +29,7 @@ import androidx.annotation.NonNull; import java.util.Arrays; import java.util.Locale; +import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; @@ -44,6 +46,8 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics; */ public class VoiceAnnouncement { + public final static int AUDIO_STREAM = TextToSpeech.Engine.DEFAULT_STREAM; + private static final String TAG = VoiceAnnouncement.class.getSimpleName(); private final Context context; @@ -79,7 +83,7 @@ public class VoiceAnnouncement { private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() { @Override public void onStart(String utteranceId) { - int result = audioManager.requestAudioFocus(audioFocusChangeListener, TextToSpeech.Engine.DEFAULT_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."); } @@ -107,11 +111,12 @@ public class VoiceAnnouncement { private boolean ttsReady = false; + private MediaPlayer ttsFallback; + VoiceAnnouncement(Context context) { this.context = context; audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); contentProviderUtils = new ContentProviderUtils(context); - } public void start() { @@ -125,6 +130,11 @@ public class VoiceAnnouncement { ttsInitStatus = status; }); } + if (ttsFallback == null) { + ttsFallback = MediaPlayer.create(context, R.raw.tts_fallback); + ttsFallback.setAudioStreamType(AUDIO_STREAM); + ttsFallback.setLooping(false); + } } public void announce(@NonNull Track track) { @@ -135,18 +145,22 @@ public class VoiceAnnouncement { onTtsReady(); } } - if (!ttsReady) { - Log.i(TAG, "TTS not ready."); - return; - } } if (Arrays.asList(AudioManager.MODE_IN_CALL, AudioManager.MODE_IN_COMMUNICATION) .contains(audioManager.getMode())) { - Log.i(TAG, "Speech is not allowed at this time."); + Log.i(TAG, "Announcement is not allowed at this time."); return; } + if (!ttsReady) { + Log.i(TAG, "TTS not ready/available, just generating a tone."); + ttsFallback.seekTo(0); + ttsFallback.start(); + return; + } + + boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context); boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, track.getCategory()); Distance minGPSDistance = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); @@ -158,7 +172,9 @@ public class VoiceAnnouncement { IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); String announcement = AnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval); - speakAnnouncement(announcement); + + // 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 shutdown() { @@ -167,6 +183,11 @@ public class VoiceAnnouncement { tts = null; } + if (ttsFallback != null) { + ttsFallback.release(); + ttsFallback = null; + } + sharedPreferences = null; } @@ -185,9 +206,4 @@ public class VoiceAnnouncement { tts.setSpeechRate(PreferencesUtils.getVoiceSpeedRate(PreferencesUtils.getSharedPreferences(context), context)); tts.setOnUtteranceProgressListener(utteranceListener); } - - private void speakAnnouncement(String announcement) { - // 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"); - } } diff --git a/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncementManager.java b/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncementManager.java index 3be013d0c..64e90ed30 100644 --- a/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncementManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/tasks/VoiceAnnouncementManager.java @@ -18,6 +18,7 @@ package de.dennisguse.opentracks.services.tasks; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.time.Duration; @@ -53,8 +54,12 @@ public class VoiceAnnouncementManager { private boolean metricUnits; private TrackStatistics trackStatistics; - private Distance nextTotalDistance = Distance.of(Double.MAX_VALUE); - private Duration nextTotalTime = Duration.ofSeconds(Long.MAX_VALUE); + + private static final Distance DISTANCE_OFF = Distance.of(Double.MAX_VALUE); + private Distance nextTotalDistance = DISTANCE_OFF; + + private static final Duration TOTALTIME_OFF = Duration.ofMillis(Long.MAX_VALUE); + private Duration nextTotalTime = TOTALTIME_OFF; public VoiceAnnouncementManager(@NonNull TrackRecordingService trackRecordingService) { this.trackRecordingService = trackRecordingService; @@ -63,7 +68,7 @@ public class VoiceAnnouncementManager { taskFrequency = TASK_FREQUENCY_OFF; } - public void restore(TrackStatistics trackStatistics) { + public void restore(@Nullable TrackStatistics trackStatistics) { if (taskFrequency == TASK_FREQUENCY_OFF) { Log.d(TAG, "Task frequency is off."); return; @@ -73,7 +78,7 @@ public class VoiceAnnouncementManager { voiceAnnouncement.start(); if (isTimeFrequency()) { - nextTotalTime = calculateNextDuration(trackStatistics); + nextTotalTime = trackStatistics != null ? calculateNextDuration(trackStatistics) : TOTALTIME_OFF; } else { // For distance periodic task updateNextTaskDistance(trackStatistics); @@ -81,6 +86,10 @@ public class VoiceAnnouncementManager { } public void update(@NonNull Track track) { + if (voiceAnnouncement == null) { + Log.e(TAG, "Cannot update when in status shutdown."); + return; + } boolean announce = false; this.trackStatistics = track.getTrackStatistics(); if (trackStatistics.getTotalDistance().greaterThan(nextTotalDistance)) { @@ -93,7 +102,6 @@ public class VoiceAnnouncementManager { } if (announce) { - this.trackStatistics = track.getTrackStatistics(); voiceAnnouncement.announce(track); } } @@ -117,7 +125,7 @@ public class VoiceAnnouncementManager { private void updateNextTaskDistance(TrackStatistics trackStatistics) { if (!isDistanceFrequency()) { - nextTotalDistance = Distance.of(Double.MAX_VALUE); + nextTotalDistance = DISTANCE_OFF; Log.d(TAG, "SplitManager: Distance splits disabled."); return; } @@ -138,12 +146,19 @@ public class VoiceAnnouncementManager { return announcementInterval.multipliedBy(index + 1); } - private Duration calculateNextDuration(@NonNull TrackStatistics trackStatistics) { + private Duration calculateNextDuration(@Nullable TrackStatistics trackStatistics) { + if (trackStatistics == null) { + return null; + } + if (!isTimeFrequency()) { throw new RuntimeException("Using distance frequency as time frequency is impossible."); } + Duration totalTime = trackStatistics.getTotalTime(); Duration interval = Duration.ofMinutes(taskFrequency); - return interval.minus(Duration.ofMillis(trackStatistics.getTotalTime().toMillis() % interval.toMillis())); + Duration intervalMod = Duration.ofMillis(trackStatistics.getTotalTime().toMillis() % interval.toMillis()); + + return totalTime.plus(interval.minus(intervalMod)); } private boolean isTimeFrequency() { diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java index 99c835205..df4311c88 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java @@ -98,9 +98,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity public static class PrefsFragment extends PreferenceFragmentCompat { - private TextToSpeech tts; - private int ttsInitStatus; - private SharedPreferences sharedPreferences; private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { @@ -133,23 +130,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity // Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData. private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog; - private Preference announcementsFrequency; - private Preference announcementsSpeed; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ttsInitStatus = TextToSpeech.ERROR; - tts = new TextToSpeech(getContext(), status -> { - ttsInitStatus = status; - Log.i(TAG, "TextToSpeech initialized with status " + status); - - if (announcementsFrequency != null && announcementsSpeed != null) { - updateVoiceAnnouncements(); - } - }); - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { sharedPreferences = PreferencesUtils.getSharedPreferences(getContext()); @@ -214,10 +194,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity startActivity(intent); return true; }); - - announcementsFrequency = findPreference(getString(R.string.voice_frequency_key)); - announcementsSpeed = findPreference(getString(R.string.voice_speed_rate_key)); - updateVoiceAnnouncements(); } @Override @@ -278,9 +254,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity super.onDestroy(); trackRecordingServiceConnection.unbind(getContext()); sharedPreferences = null; - announcementsFrequency = null; - announcementsSpeed = null; - tts = null; } public void setDefaultActivity(String iconValue) { @@ -374,12 +347,6 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity }); } - private void updateVoiceAnnouncements() { - boolean enabled = ttsInitStatus == TextToSpeech.SUCCESS; - announcementsFrequency.setEnabled(enabled); - announcementsSpeed.setEnabled(enabled); - } - private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) { this.recordingStatus = status; if (!status.isRecording() && isAdded()) { diff --git a/src/main/java/de/dennisguse/opentracks/util/TTSUtils.java b/src/main/java/de/dennisguse/opentracks/util/TTSUtils.java deleted file mode 100644 index deb95b3e8..000000000 --- a/src/main/java/de/dennisguse/opentracks/util/TTSUtils.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.dennisguse.opentracks.util; - -import android.speech.tts.TextToSpeech; - -public class TTSUtils { - - private TTSUtils() { - } - - public static int getTTSStream() { - return TextToSpeech.Engine.DEFAULT_STREAM; - } -} diff --git a/src/main/res/raw/tts_fallback.mp3 b/src/main/res/raw/tts_fallback.mp3 new file mode 100644 index 000000000..6a2f78c00 Binary files /dev/null and b/src/main/res/raw/tts_fallback.mp3 differ