I am writing an Android
app to fetch the latest email from a folder and play it using TTS. I want to be able to use it whilst driving so it has to be mostly automatic. Everything so far is working fine until I attempt to capture when the TextToSpeech
has finished speaking so we can move on to the next email.
Here is the complete MainActivity.java
file:
package uk.co.letsdelight.emailreader;
import android.os.AsyncTask;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.util.Properties;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.internet.MimeBodyPart;
public class MainActivity extends AppCompatActivity implements TextToSpeech.OnInitListener {
public TextToSpeech tts;
private Bundle ttsParam = new Bundle();
public UtteranceProgressListener utListener;
private boolean isPlaying = false;
private Properties imap = new Properties();
private String textToSpeak = "";
@Override
public void onInit(int ttsStatus) {
if (ttsStatus == TextToSpeech.SUCCESS) {
utListener = new UtteranceProgressListener() {
@Override
public void onStart(String s) {
TextView status = findViewById(R.id.status);
status.setText("started reading (Listener)");
}
@Override
public void onDone(String s) {
Toast.makeText(getApplicationContext(), "Done Event Listener", Toast.LENGTH_LONG).show();
TextView status = findViewById(R.id.status);
status.setText("finished reading (Listener)");
/*ImageButton i = findViewById(R.id.playButton);
i.setImageResource(R.drawable.button_play);*/
isPlaying = false;
}
@Override
public void onStop(String s, boolean b) {
Toast.makeText(getApplicationContext(), "Stop Event Listener", Toast.LENGTH_LONG).show();
TextView status = findViewById(R.id.status);
status.setText("stopped reading (Listener)");
/*ImageButton i = findViewById(R.id.playButton);
i.setImageResource(R.drawable.button_play);*/
isPlaying = false;
}
@Override
public void onError(String s) {
Toast.makeText(getApplicationContext(), "Error Event Listener", Toast.LENGTH_LONG).show();
TextView status = findViewById(R.id.status);
status.setText("Error reading email");
ImageButton i = findViewById(R.id.playButton);
i.setImageResource(R.drawable.button_play);
isPlaying = false;
}
};
tts.setOnUtteranceProgressListener(utListener);
TextView status = findViewById(R.id.status);
status.setText("initialised");
} else {
TextView status = findViewById(R.id.status);
status.setText("failed to initialise");
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
imap.setProperty("mail.store.protocol", "imap");
imap.setProperty("mail.imaps.port", "143");
tts = new TextToSpeech(this,this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
public void restartPressed(View v) {
if (isPlaying) {
tts.stop();
speak();
}
}
public void playPressed(View v) {
ImageButton i = (ImageButton) v;
if (isPlaying) {
isPlaying = false;
i.setImageResource(R.drawable.button_play);
TextView status = findViewById(R.id.status);
status.setText("");
if (tts != null) {
tts.stop();
}
} else {
isPlaying = true;
i.setImageResource(R.drawable.button_stop);
new Reader().execute();
}
}
class Reader extends AsyncTask<String, Void, String> {
@Override
protected void onPreExecute() {
super.onPreExecute();
TextView status = findViewById(R.id.status);
status.setText("fetching email");
}
@Override
protected String doInBackground(String... params) {
String toRead = "nothing to fetch";
try {
Session session = Session.getDefaultInstance(imap, null);
Store store = session.getStore();
store.connect(getText(R.string.hostname).toString(), getText(R.string.username).toString(), getText(R.string.password).toString());
Folder inbox = store.getFolder("INBOX.Articles.listen");
if (inbox.exists() && inbox.getMessageCount() > 0) {
inbox.open(Folder.READ_ONLY);
Message msg = inbox.getMessage(inbox.getMessageCount() - 6);
if (msg.getContentType().contains("multipart")) {
Multipart multiPart = (Multipart) msg.getContent();
MimeBodyPart part = (MimeBodyPart) multiPart.getBodyPart(multiPart.getCount() - 1);
toRead = part.getContent().toString();
} else {
toRead = msg.getContent().toString();
}
} else {
toRead = "The folder is empty or doesn't exist";
}
} catch (Throwable ex) {
toRead = "Error fetching email - " + ex.toString();
}
return toRead;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
String body;
TextView status = findViewById(R.id.status);
status.setText("");
try {
Document doc = Jsoup.parse(s);
body = doc.body().text();
} catch (Throwable ex) {
body = "Error parsing email - " + ex.toString();
}
status.setText("email successfully fetched");
textToSpeak = body;
if (isPlaying) {
speak();
}
}
}
private void speak() {
int maxLength = TextToSpeech.getMaxSpeechInputLength();
if (textToSpeak.length() > maxLength) {
textToSpeak = "The email text is too long! The maximum length is " + maxLength + " characters";
}
ttsParam.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "EmailReader");
tts.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, ttsParam, "EmailReader");
}
@Override
protected void onDestroy() {
if (tts != null) {
tts.stop();
tts.shutdown();
}
super.onDestroy();
}
}
The inner class Reader
works fine. doInBackground
fetches the email and onPostExec
strips out any HTML to leave the actual text content of the email. This is passed to the speak()
method which does the actual speaking and works.
The issue is with the onUtteranceProgressListener
.
Sometimes the onStart(String s)
method is called, sometimes it isn't! It seems to never be called the first time the email read out. Mostly it is called for subsequent calls to speak()
but not always. About 1 in 5 times it fails to get called. If the listener is called the status displays 'started reading (Listener)' otherwise it shows 'email successfully fetched'.
onDone
, onError
and onStop
are never called.
I have tried using different utteranceID
and Bundle
values in the tts.speak()
call but this makes to difference.
When the app is started, the first status display is 'initialised' which means that the onUtteranceListener
must have been set within the onInit
method. The TextToSpeech
object is instantiated within the activity's onCreate
method.
I have looked through all the information I can find which has mostly suggested getting the utteranceID
correct. What else can I try in order to get a better understanding of this problem please?