Skip to content

Commit 35dd264

Browse files
authored
Merge pull request #35 from Julius-Babies/34-app-is-stuck-loading-or-crashes-on-startup-across-android-13-grapheneos-pixel-8-and-stock-pixel-7
#34 Fix App crash if TTS engine is unavailable
2 parents c63df8b + b10f966 commit 35dd264

7 files changed

Lines changed: 128 additions & 6 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
66
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
77
<uses-permission android:name="android.permission.INTERNET" />
8+
<uses-permission android:name="android.permission.VIBRATE" />
89

910
<application
1011
android:allowBackup="true"
@@ -28,5 +29,4 @@
2829
</intent-filter>
2930
</activity>
3031
</application>
31-
32-
</manifest>
32+
</manifest>

app/src/main/java/org/jugendhackt/wegweiser/tts/TTS.kt

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
package org.jugendhackt.wegweiser.tts
22

33
import android.content.Context
4+
import android.media.MediaPlayer
5+
import android.os.Build
6+
import android.os.VibrationEffect
7+
import android.os.Vibrator
48
import android.speech.tts.TextToSpeech
59
import android.speech.tts.TextToSpeech.OnInitListener
610
import android.speech.tts.UtteranceProgressListener
711
import android.util.Log
12+
import androidx.core.content.getSystemService
813
import java.util.Locale
914
import org.jugendhackt.wegweiser.language.language
1015

1116
class 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
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import json
5+
from pathlib import Path
6+
7+
from gtts import gTTS
8+
9+
MESSAGES_PATH = Path("tts_unavailable_messages.json")
10+
OUTPUT_DIR = Path(".")
11+
FILE_PREFIX = "tts_unavailable"
12+
SLOW = False
13+
14+
15+
def main() -> int:
16+
# utf-8-sig handles normal UTF-8 files and UTF-8 with BOM.
17+
data = json.loads(MESSAGES_PATH.read_text(encoding="utf-8-sig"))
18+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
19+
20+
for lang, text in data.items():
21+
# Output example: tts_unavailable_de.mp3
22+
out_file = OUTPUT_DIR / f"{FILE_PREFIX}_{lang}.mp3"
23+
tts = gTTS(text=text.strip(), lang=lang, slow=SLOW)
24+
tts.save(str(out_file))
25+
print(f"Generated: {out_file}")
26+
27+
return 0
28+
29+
30+
if __name__ == "__main__":
31+
raise SystemExit(main())
39.9 KB
Binary file not shown.
40.7 KB
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"de": "Hallo. Die Sprachausgabe ist auf diesem Gerät gerade nicht verfügbar.",
3+
"en": "Hi there. Text to speech is currently unavailable on this device."
4+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
agp = "9.2.0"
2+
agp = "9.2.1"
33
kotlin = "2.3.21"
44
coreKtx = "1.18.0"
55
junit = "4.13.2"

0 commit comments

Comments
 (0)