From 5693cfcbea88040e38f3313346e11815f9490b90 Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Mon, 13 Jul 2026 15:18:12 +0100 Subject: [PATCH] Fix main-thread ANR from TTS getVoices() binder call TextToSpeech.getVoices() makes a synchronous Binder IPC to the system TTS service, which can block for a long time on some OEM builds. It was being called from viewModelScope's default Main dispatcher, blocking the UI thread and triggering ANRs. Move it and getAvailableSpeechEngines() to Dispatchers.IO. Co-Authored-By: Claude Sonnet 5 --- .../soundscape/viewmodels/SettingsViewModel.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/SettingsViewModel.kt b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/SettingsViewModel.kt index d227f03ae..d7048a1a2 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/viewmodels/SettingsViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.scottishtecharmy.soundscape.MainActivity import org.scottishtecharmy.soundscape.SoundscapeServiceConnection import org.scottishtecharmy.soundscape.screens.onboarding.audiobeacons.getBeaconResourceId @@ -90,9 +92,16 @@ class SettingsViewModel @Inject constructor( if (initialized) { // Only once the TextToSpeech engine is initialized can we populate the // members of these lists. - val audioEngineTypes = audioEngine.getAvailableSpeechEngines() - - val audioEngineVoiceTypes = audioEngine.getAvailableSpeechVoices() + // getAvailableSpeechEngines/getAvailableSpeechVoices end up making a + // synchronous Binder call into the system TTS service + // (ITextToSpeechService.getVoices), which can block for a long time on + // some OEM builds. Run them off the main thread to avoid ANRs. + val (audioEngineTypes, audioEngineVoiceTypes) = withContext(Dispatchers.IO) { + Pair( + audioEngine.getAvailableSpeechEngines(), + audioEngine.getAvailableSpeechVoices() + ) + } val voiceTypes = mutableListOf() // The list of voices will start of with those in the current locale