未可靠地调用 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'.

onDoneonErroronStop 永远不会被调用。

我曾尝试在 tts.speak() 调用中使用不同的 utteranceIDBundle 值,但这会有所不同。

当应用程序启动时,第一个状态显示是'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()。