Voice announcements: if TTS is not available play a sound file.

Fixes #862.
This commit is contained in:
Dennis Guse
2021-08-26 22:50:38 +02:00
parent 4f31607e3e
commit fa02ba9c69
7 changed files with 57 additions and 72 deletions
@@ -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());
@@ -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));
}
@@ -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");
}
}
@@ -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() {
@@ -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()) {
@@ -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;
}
}