未可靠地调用 UtteranceProgressListener
UtteranceProgressListener not being reliably called
我正在编写一个 Android
应用程序来从文件夹中获取最新的电子邮件并使用 TTS 播放。我希望能够在开车时使用它,所以它必须大部分是自动的。到目前为止一切正常,直到我尝试捕捉 TextToSpeech
结束讲话的时间,以便我们可以继续处理下一封电子邮件。
这是完整的 MainActivity.java
文件:
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();
}
}
内部 class Reader
工作正常。 doInBackground
获取电子邮件并 onPostExec
删除任何 HTML 以保留电子邮件的实际文本内容。这被传递给 speak()
方法,该方法进行实际的说话和工作。
问题出在 onUtteranceProgressListener
。
有时会调用 onStart(String s)
方法,有时不会!它似乎永远不会在第一次读出电子邮件时被调用。大多数情况下,它会在后续调用 speak()
时被调用,但并非总是如此。大约五分之一的时间它没有被调用。如果调用侦听器,则状态显示 'started reading (Listener)' 否则显示 'email successfully fetched'.
onDone
、onError
和 onStop
永远不会被调用。
我曾尝试在 tts.speak()
调用中使用不同的 utteranceID
和 Bundle
值,但这会有所不同。
当应用程序启动时,第一个状态显示是'initialised',这意味着onUtteranceListener
必须在onInit
方法中设置。 TextToSpeech
对象在 activity 的 onCreate
方法中实例化。
我查看了所有我能找到的信息,其中大部分建议 utteranceID
正确。为了更好地理解这个问题,我还能尝试什么?
首先,确保您确实遇到了问题,而不是谁按什么顺序设置文本之间的竞争。使用日志语句来确保它实际上没有被调用。
尝试将 queueMode 设置为 QUEUE_ADD,例如:
tts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, ttsParam, "EmailReader");
可能后续调用正在取消来自先前文本输入的侦听器事件,正如 QUEUE_FLUSH 所建议的那样。
此外,这里并不真正需要捆绑包,您可以将其设置为空。
希望这些对您有所帮助。
问题是 onDone() 方法(实际上是任何进度回调)在后台线程上 运行,因此 Toast 将无法工作,任何访问的代码您的 UI 例如 setText(...) 可能有效也可能无效。
所以...这些方法可能 正在 被调用,但您看不到它。
解决方法是用 运行OnUiThread() 包围回调中的代码,如下所示:
@Override
public void onDone(String s) {
runOnUiThread(new Runnable() {
@Override
public void run() {
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;
}
});
}
注意:最好在 onCreate() 中初始化您的 TextView 以及其他所有内容,而不是在进度回调中。
此外,utteranceID 的目的是为每次调用 speak() 提供一个唯一标识符,然后作为进度回调中的 "String s" 参数传回给您。
最好使用某种随机数生成器为每个通话提供一个新的 ("most recent") ID,然后在进度回调中检查它。
您可以看到与此相关的类似问答 。
旁注:
因为你有一个 "restart" 按钮,你应该知道在 <23 的 API 上,调用 TextToSpeech.stop() 将导致你的进度监听器中的 onDone() 被调用。在 API 23+ 上,它改为调用 onStop()。
我正在编写一个 Android
应用程序来从文件夹中获取最新的电子邮件并使用 TTS 播放。我希望能够在开车时使用它,所以它必须大部分是自动的。到目前为止一切正常,直到我尝试捕捉 TextToSpeech
结束讲话的时间,以便我们可以继续处理下一封电子邮件。
这是完整的 MainActivity.java
文件:
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();
}
}
内部 class Reader
工作正常。 doInBackground
获取电子邮件并 onPostExec
删除任何 HTML 以保留电子邮件的实际文本内容。这被传递给 speak()
方法,该方法进行实际的说话和工作。
问题出在 onUtteranceProgressListener
。
有时会调用 onStart(String s)
方法,有时不会!它似乎永远不会在第一次读出电子邮件时被调用。大多数情况下,它会在后续调用 speak()
时被调用,但并非总是如此。大约五分之一的时间它没有被调用。如果调用侦听器,则状态显示 'started reading (Listener)' 否则显示 'email successfully fetched'.
onDone
、onError
和 onStop
永远不会被调用。
我曾尝试在 tts.speak()
调用中使用不同的 utteranceID
和 Bundle
值,但这会有所不同。
当应用程序启动时,第一个状态显示是'initialised',这意味着onUtteranceListener
必须在onInit
方法中设置。 TextToSpeech
对象在 activity 的 onCreate
方法中实例化。
我查看了所有我能找到的信息,其中大部分建议 utteranceID
正确。为了更好地理解这个问题,我还能尝试什么?
首先,确保您确实遇到了问题,而不是谁按什么顺序设置文本之间的竞争。使用日志语句来确保它实际上没有被调用。
尝试将 queueMode 设置为 QUEUE_ADD,例如:
tts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, ttsParam, "EmailReader");
可能后续调用正在取消来自先前文本输入的侦听器事件,正如 QUEUE_FLUSH 所建议的那样。
此外,这里并不真正需要捆绑包,您可以将其设置为空。
希望这些对您有所帮助。
问题是 onDone() 方法(实际上是任何进度回调)在后台线程上 运行,因此 Toast 将无法工作,任何访问的代码您的 UI 例如 setText(...) 可能有效也可能无效。
所以...这些方法可能 正在 被调用,但您看不到它。
解决方法是用 运行OnUiThread() 包围回调中的代码,如下所示:
@Override
public void onDone(String s) {
runOnUiThread(new Runnable() {
@Override
public void run() {
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;
}
});
}
注意:最好在 onCreate() 中初始化您的 TextView 以及其他所有内容,而不是在进度回调中。
此外,utteranceID 的目的是为每次调用 speak() 提供一个唯一标识符,然后作为进度回调中的 "String s" 参数传回给您。
最好使用某种随机数生成器为每个通话提供一个新的 ("most recent") ID,然后在进度回调中检查它。
您可以看到与此相关的类似问答
旁注:
因为你有一个 "restart" 按钮,你应该知道在 <23 的 API 上,调用 TextToSpeech.stop() 将导致你的进度监听器中的 onDone() 被调用。在 API 23+ 上,它改为调用 onStop()。