forked from upstream-mirrors/OpenTracks
Voice announcements: if TTS is not available play a sound file.
Fixes #862.
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user