【问题标题】:UtteranceProgressListener not being reliably called没有可靠地调用 UtteranceProgressListener
【发布时间】:2019-02-27 03:43:13
【问题描述】:

我正在编写一个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();
}
}

内部类Reader 工作正常。 doInBackground 获取电子邮件,onPostExec 删除任何 HTML 以保留电子邮件的实际文本内容。这被传递给 speak() 方法,该方法执行实际的说话和工作。

问题在于onUtteranceProgressListener

有时会调用onStart(String s) 方法,有时则不会!电子邮件第一次读出时似乎永远不会被调用。大多数情况下,它会在随后调用speak() 时被调用,但并非总是如此。大约五分之一的时间它没有被调用。如果调用了侦听器,则状态显示“开始阅读(侦听器)”,否则显示“成功获取电子邮件”。

onDoneonErroronStop 永远不会被调用。

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

当应用程序启动时,第一个状态显示为“已初始化”,这意味着onUtteranceListener 必须已在onInit 方法中设置。 TextToSpeech 对象在 Activity 的 onCreate 方法中实例化。

我查看了我能找到的所有信息,其中大部分建议正确设置utteranceID。为了更好地理解这个问题,我还能尝试什么?

【问题讨论】:

    标签: android listener text-to-speech


    【解决方案1】:

    问题在于 onDone() 方法(实际上是任何进度回调)在后台线程上运行,因此 Toast 无法正常工作,任何访问您的 UI 的代码(例如 setText( ...) 可能有效,也可能无效。

    所以...这些方法可能正在被调用,但你只是看不到。

    解决方案是使用 runOnUiThread() 围绕回调中的代码,如下所示:

    @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”参数传回给您。

    最好使用某种随机数生成器为每次通话提供一个新的(“最近的”)ID,然后在进度回调中检查它。

    您可以看到关于此here 的类似问题和答案。

    旁注:

    由于您有一个“重新启动”按钮,您应该知道在 API

    【讨论】:

    • 谢谢你,这确实推动了我前进。尽管需要对您的代码稍作更改。侦听器需要在 runnable() 的 run() 方法中:您能说明为什么 onStart() 侦听器有时会起作用,而其他时候不起作用?
    • 该应用程序是为 API 26 构建的,因为我的手机运行的是 Android 8.0,它不适合一般分发。虽然我在 IDE 中指定了最低 API 23,因为我正在 Android 7.0 设备上进行测试。
    • 哦,糟糕!抱歉……修好了。关于 onStart()(或任何回调),我的理论是相同的——它似乎有时只起作用,因为对 UI 线程的后台调用有时确实起作用……但主要是不要......但如果你使用 runOnUiThread(...) 总是可以工作。
    • runOnUiThread 中包装代码的相同技术是否可以在doInBackgroundReader 方法中工作?我需要保留电子邮件消息的句柄,以便在 TTS 读出内容后删除或移动它
    • 我不确定...应该有更好的方法来实现这一点。我建议对此提出一个新问题。
    【解决方案2】:

    首先,请确保您确实遇到了问题,而不是在谁以什么顺序设置文本之间存在竞争。使用日志语句确保它实际上没有被调用。

    尝试将 queueMode 设置为 QUEUE_ADD,例如:

    tts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, ttsParam, "EmailReader");
    

    正如 QUEUE_FLUSH 所暗示的那样,后续调用可能会从先前的文本输入中取消侦听器的事件。

    此外,那里并不真正需要捆绑包,您可以将其设置为 null。

    希望这些对您有所帮助。

    【讨论】:

    • 谢谢 - 如果这里不需要 Bundle,什么时候需要?
    猜你喜欢
    • 2017-08-15
    • 2018-06-23
    • 2013-12-16
    • 2022-11-03
    • 2014-08-06
    • 2020-09-16
    • 1970-01-01
    • 1970-01-01
    • 2016-12-26
    相关资源
    最近更新 更多