2

I am working on an app that needs to keep reading aloud text after the screen is turned off. To achieve this goal, I put the Text-to-speech (TTS) code in Foreground Service, so the TTS can keep running when the screen is off.

It worked well on my phone before. But after I upgraded my phone from Android 11 to Android 12, the TTS stops working after the screen is turned off for a while, usually after several minutes.

Normally, after the TTS finishes speaking one sentence, it will call the onDone method of the UtteranceProgressListener, so I can make the TTS speak next sentence there. The reason the TTS stops working is that the onDone method stops getting called after the screen is turned off for a while. It doesn't stop immediately, but stops after a few minutes, sometimes longer, sometimes shorter.

I guessed the battery optimization of the new Android OS causes this problem. But after I turned off the system battery optimization, it doesn't work either. I also noticed that some similar apps have the same problem but some apps don't. How can I solve this problem?

Denny Hsu
  • 163
  • 7

1 Answers1

1

This code is working in Android 12 even app is background

class TTS : Service(), OnInitListener {

private var tts: TextToSpeech? = null
private lateinit var spokenText: String
private var isInit: Boolean = false

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if(intent?.extras != null) {
        spokenText = intent.getStringExtra("text").toString()
    }
    else {
        spokenText = ""
    }
    Log.d(TAG, "onStartCommand: $spokenText")
    return START_NOT_STICKY
}

override fun onCreate() {
    tts = TextToSpeech(this, this)
    Log.d(TAG, "onCreate: CREATING AGAIN !!")
}

override fun onInit(status: Int) {
    if (status == TextToSpeech.SUCCESS) {
        Log.d(TAG, "onInit: TextToSpeech Success")
        val result = tts!!.setLanguage(Locale("hi", "IN"))
        if (result != TextToSpeech.LANG_MISSING_DATA && result != TextToSpeech.LANG_NOT_SUPPORTED) {
            Log.d(TAG, "onInit: speaking........")
            addAudioAttributes()
            isInit = true
        }
    }
    else {
        Log.d(TAG, "onInit: TTS initialization failed")
        Toast.makeText(
            applicationContext,
            "Your device don't support text to speech.\n Visit app to download!!",
            Toast.LENGTH_SHORT
        ).show()
    }
}

private fun addAudioAttributes() {
    val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val audioAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build()
        tts?.setAudioAttributes(audioAttributes)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val focusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .build()
                )
                .setAcceptsDelayedFocusGain(true)
                .setOnAudioFocusChangeListener { focus ->
                    when (focus) {
                        AudioManager.AUDIOFOCUS_GAIN -> {
                        }
                        else -> stopSelf()
                    }
                }.build()

        when (audioManager.requestAudioFocus(focusRequest)) {
            AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> speak(audioManager, focusRequest)
            AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> stopSelf()
            AudioManager.AUDIOFOCUS_REQUEST_FAILED -> stopSelf()
        }

    } else {
        val result = audioManager.requestAudioFocus( { focusChange: Int ->
            when(focusChange) {
                AudioManager.AUDIOFOCUS_GAIN -> {
                }
                else -> stopSelf()
            }
        },
            AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
        )

        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            speak(audioManager, null)
        }
    }
}

private fun speak(audioManager: AudioManager, focusRequest: AudioFocusRequest?) {
    val speechListener = object : UtteranceProgressListener() {
        override fun onStart(utteranceId: String?) {
            Log.d(TAG, "onStart: Started syntheses.....")
        }

        override fun onDone(utteranceId: String?) {
            Log.d(TAG, "onDone: Completed synthesis ")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest)
            }
            stopSelf()
        }

        override fun onError(utteranceId: String?) {
            Log.d(TAG, "onError: Error synthesis")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest)
            }
            stopSelf()
        }
    }
    val paramsMap: HashMap<String, String> = HashMap()
    paramsMap[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "tts_service"

    tts?.speak(spokenText, TextToSpeech.QUEUE_ADD, paramsMap)
    tts?.setOnUtteranceProgressListener(speechListener)
}

override fun onDestroy() {
    if (tts != null) {
        Log.d(TAG, "onDestroy: destroyed tts")
        tts?.stop()
        tts?.shutdown()
    }
    super.onDestroy()
}

override fun onBind(arg0: Intent?): IBinder? {
    return null
}

companion object {
    private const val TAG = "TTS_Service"
}

}

  • 1
    I compared your code and found two key differences. You implement ```OnInitListener``` to the class and put ```this``` at the second parameter of ```TextToSpeech(this, this)```. I directly create an object in the function parentheses. Second, you create an ```UtteranceProgressListener``` first and put it in ```setOnUtteranceProgressListener()```. I directly create an object in the function parentheses. I change the coding style to be like yours, and I also need to delete a ```CountDownTimer``` which is also running in the Foreground Service. Now it works perfectly. Thank you for your help! – Denny Hsu May 10 '22 at 07:00
  • 1
    I did more tests today and found that I also need to add ```setAudioAttributes()``` to my code to prevent TTS from stopping. It seems that there are multiple reasons that TTS stops when screen is off. – Denny Hsu May 11 '22 at 15:17
  • The TTS object should only be created in ```onCreate()```, or it will stop after screen is off for a while. – Denny Hsu May 13 '22 at 08:12
  • I found that I didn't really disable the battery optimization before. After I disabled the battery optimization, the problem was solved. Now I can keep the ```CountDownTimer``` and create TTS at any place. The good coding style can make the TTS live longer but not forever, if the battery optimization is too aggressive. This post is the method to really disable the battery optimization. [Check if battery optimization is enabled or not for an app](https://stackoverflow.com/questions/39256501/check-if-battery-optimization-is-enabled-or-not-for-an-app) – Denny Hsu May 14 '22 at 10:23