I\'m trying to use TextToSpeech in my app.
I wrote it as a bound service. Problem appears when I need to read text before activity stops. It says just half of text
Instead of just bind service, start your service using startService()
and then bind. After reading the text in onUtteranceCompleted()
call stopSelf()
.
Here is my implementation - as inspired by Hoan's answer (and added many years later but hopefully useful to someone else to save them having to work it out as I did.
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Queue;
import timber.log.Timber;
/**
* This is a simplified text to speech service where each "instance" (each time startService is
* called) is only expected to speak once (we don't check this but the service gets stopped after it
* has spoken once). This was created for the case where we want an announcement to be made just
* before finishing an activity so couldn't do it in the activity itself because it gets destroyed
* before it finishes speaking.
*
* <p>Usage (in the activity that "speaks"):
* <li>Check there is text to speech data available on the device. See:
* https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html
* <li>Call startService after successfully checking for speech data
* <li>Bind the activity to the service onStart (and unbind onStop). Getting an instance of the service to later pass
* the text to speak though to.
* <li> call `.speak(textToSpeak)` when ready to speak (after which we cannot call `.speak()` again)
*
* <p>NB: We assume that the activity that started this service checks that TTS data is
* available.
* <p>NB: You need to start the service a few seconds before asking it to speak as the text to speech api doesn't
* start immediately.
*/
public class TTSSingleUtteranceService extends Service {
private boolean initDone = false;
private TextToSpeech textToSpeech;
Queue<Integer> startIds = new LinkedList<>();
// Binder given to clients
private final IBinder binder = new TTSBinder();
/**
* Class used for the client Binder. Because we know this service always runs in the same process
* as its clients, we don't need to deal with IPC.
*/
public class TTSBinder extends Binder {
public TTSSingleUtteranceService getService() {
// Return this instance of TTSService so clients can call public methods
return TTSSingleUtteranceService.this;
}
}
@Override
public void onCreate() {
super.onCreate();
// Initialise the TextToSpeech object
textToSpeech =
new TextToSpeech(
this,
status -> {
if (status == TextToSpeech.SUCCESS) {
initDone = true;
Locale locale = Locale.getDefault();
int ttsLang = textToSpeech.setLanguage(locale);
if (ttsLang == TextToSpeech.LANG_MISSING_DATA
|| ttsLang == TextToSpeech.LANG_NOT_SUPPORTED) {
Timber.e("TTS: Language %s is not supported!", locale);
}
textToSpeech.setOnUtteranceProgressListener(
new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
Timber.i("Utterance started");
}
@Override
public void onDone(String utteranceId) {
Timber.i("Utterance completed. Shutting down service");
if (!startIds.isEmpty()) {
stopSelf(startIds.remove());
} else {
stopSelf();
}
}
@Override
public void onError(String utteranceId) {
Timber.i("Utterance errored");
}
});
Timber.i("TTS: Initialization success with locale=%s.", locale);
} else {
Timber.e("Text to speech initialisation failed");
}
});
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// add this startId to the queue of ids so that we can shutdown each one in order
startIds.add(startId);
// If we get killed, after returning from here, do not recreate the service unless there are
// pending intents to deliver.
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
Timber.i("Destroying TTS service");
if (textToSpeech != null) {
textToSpeech.shutdown();
}
super.onDestroy();
}
/** method for clients */
public void speak(String text) {
Timber.i("speak called with text=%s", text);
if (text != null && initDone) {
String utteranceId = String.valueOf(text.hashCode());
textToSpeech.speak(text, TextToSpeech.QUEUE_ADD, null, utteranceId);
} else {
Timber.e("TTSError: textToSpeech is null or hasn't finished initialising!");
}
}
}
I then use this from the Android Activity like so
public class MyActivity extends AppCompatActivity {
TTSSingleUtteranceService ttsService;
boolean boundToTtsService = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check there is text to speech data available on the device and then will start the text to
// speech service in onActivityResult
Intent checkIntent = new Intent();
checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
startActivityForResult(checkIntent, TTS_CHECK_DATA_AVAILABLE);
}
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, TTSSingleUtteranceService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(connection);
boundToTtsService = false;
}
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection connection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
// We've bound to TTSService, cast the IBinder and get the TTSService instance
TTSSingleUtteranceService.TTSBinder binder = (TTSSingleUtteranceService.TTSBinder) service;
ttsService = binder.getService();
boundToTtsService = true;
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
boundToTtsService = false;
}
};
public void saveClicked() {
// this is called to save the data entered and then speak the results and finish the activity
// perform needed logic
// Speak text
if (boundToTtsService) {
ttsService.speak("This is the text that will be spoken");
}
finish();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == TTS_CHECK_DATA_AVAILABLE) {
if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
// success, start the TTS service
startService(new Intent(this, TTSSingleUtteranceService.class));
} else {
// missing data, install it
Timber.i("TTS: Missing text to speech resources - redirecting user to install them.");
Intent installIntent = new Intent();
installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
startActivity(installIntent);
}
}
}
}
Finally don't forget to add the service to your AndroidManifest.xml
(within the application section)
<service android:name=".util.TTSSingleUtteranceService" />