11package org.jugendhackt.wegweiser.tts
22
33import android.content.Context
4+ import android.media.MediaPlayer
5+ import android.os.Build
6+ import android.os.VibrationEffect
7+ import android.os.Vibrator
48import android.speech.tts.TextToSpeech
59import android.speech.tts.TextToSpeech.OnInitListener
610import android.speech.tts.UtteranceProgressListener
711import android.util.Log
12+ import androidx.core.content.getSystemService
813import java.util.Locale
914import org.jugendhackt.wegweiser.language.language
1015
1116class TTS (context : Context ) {
1217
18+ private val context = context.applicationContext
1319 private val textToSpeech: TextToSpeech
20+ private val vibrator: Vibrator ? = context.getSystemService()
21+ private var fallbackPlayer: MediaPlayer ? = null
1422 private var isSpeaking = false
15- private val language = language(context)
23+ private var isInitialized = false
24+ private var hasPlayedUnavailableNotice = false
25+ private val language = language(this @TTS.context)
26+ private val forceTtsUnavailableForTesting = false
1627
1728 init {
18- textToSpeech = TextToSpeech (context, OnInitListener { status ->
29+ textToSpeech = TextToSpeech (this @TTS. context, OnInitListener { status ->
1930 if (status == TextToSpeech .SUCCESS ) {
2031 val langResult = textToSpeech.setLanguage(
2132 if (language.getLanguage() == " de" ) Locale .GERMAN
@@ -24,9 +35,15 @@ class TTS(context: Context) {
2435 )
2536 if (langResult == TextToSpeech .LANG_MISSING_DATA || langResult == TextToSpeech .LANG_NOT_SUPPORTED ) {
2637 Log .e(" TTS" , " Language not supported or data missing." )
38+ isInitialized = false
39+ announceUnavailableOnStartup()
40+ } else {
41+ isInitialized = true
2742 }
2843 } else {
29- throw RuntimeException (" Failed to initialize TextToSpeech: $status " )
44+ Log .e(" TTS" , " Failed to initialize TextToSpeech: $status " )
45+ isInitialized = false
46+ announceUnavailableOnStartup()
3047 }
3148 })
3249 }
@@ -35,6 +52,21 @@ class TTS(context: Context) {
3552 * Will not speak if an output is already in progress
3653 */
3754 fun speak (text : String , onFinished : (() -> Unit )? = null) {
55+ if (forceTtsUnavailableForTesting) {
56+ Log .w(" TTS" , " TTS unavailable (forced for testing), skipping speak request." )
57+ notifyTtsUnavailable(playVoice = ! hasPlayedUnavailableNotice)
58+ hasPlayedUnavailableNotice = true
59+ onFinished?.invoke()
60+ return
61+ }
62+
63+ if (! isInitialized) {
64+ Log .w(" TTS" , " TTS not initialized, skipping speak request." )
65+ notifyTtsUnavailable(playVoice = false )
66+ onFinished?.invoke()
67+ return
68+ }
69+
3870 textToSpeech.setOnUtteranceProgressListener(object : UtteranceProgressListener () {
3971 override fun onStart (utteranceId : String? ) {}
4072
@@ -56,7 +88,62 @@ class TTS(context: Context) {
5688 }
5789
5890 fun stop () {
91+ if (! isInitialized) {
92+ return
93+ }
5994 textToSpeech.stop()
95+ fallbackPlayer?.release()
96+ fallbackPlayer = null
6097 isSpeaking = false
6198 }
99+
100+ private fun notifyTtsUnavailable (playVoice : Boolean ) {
101+ if (playVoice) {
102+ playUnavailableNotice()
103+ }
104+ try {
105+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
106+ vibrator?.vibrate(VibrationEffect .createWaveform(longArrayOf(0 , 150 , 70 , 150 ), - 1 ))
107+ } else {
108+ @Suppress(" DEPRECATION" )
109+ vibrator?.vibrate(longArrayOf(0 , 150 , 70 , 150 ), - 1 )
110+ }
111+ } catch (e: SecurityException ) {
112+ Log .w(" TTS" , " Vibration unavailable" , e)
113+ }
114+ }
115+
116+ private fun announceUnavailableOnStartup () {
117+ notifyTtsUnavailable(playVoice = true )
118+ }
119+
120+ private fun playUnavailableNotice () {
121+ val resourceName = if (language.getLanguage() == " de" ) " tts_unavailable_de" else " tts_unavailable_en"
122+ val rawResId = context.resources.getIdentifier(resourceName, " raw" , context.packageName)
123+ if (rawResId == 0 ) {
124+ Log .w(" TTS" , " Fallback voice message missing: res/raw/$resourceName " )
125+ return
126+ }
127+
128+ try {
129+ fallbackPlayer?.release()
130+ fallbackPlayer = null
131+ val player = MediaPlayer .create(context, rawResId) ? : return
132+ fallbackPlayer = player
133+ player.setOnCompletionListener {
134+ it.release()
135+ if (fallbackPlayer == = it) fallbackPlayer = null
136+ }
137+ player.setOnErrorListener { mp, _, _ ->
138+ mp.release()
139+ if (fallbackPlayer == = mp) fallbackPlayer = null
140+ true
141+ }
142+ player.start()
143+ } catch (e: Exception ) {
144+ Log .w(" TTS" , " Could not play fallback voice message" , e)
145+ fallbackPlayer?.release()
146+ fallbackPlayer = null
147+ }
148+ }
62149}
0 commit comments