简体   繁体   English

无法可靠地调用UtteranceProgressListener

[英]UtteranceProgressListener not being reliably called

I am writing an Android app to fetch the latest email from a folder and play it using TTS. 我正在编写一个Android应用程序,以从文件夹中获取最新电子邮件并使用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. 到目前为止,一切工作正常,直到我尝试捕获TextToSpeech结束的讲话后,我们才能继续阅读下一封电子邮件。

Here is the complete MainActivity.java file: 这是完整的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();
}
}

The inner class Reader works fine. 内部类Reader可以正常工作。 doInBackground fetches the email and onPostExec strips out any HTML to leave the actual text content of the email. doInBackground获取电子邮件, onPostExec删除所有HTML,以保留电子邮件的实际文本内容。 This is passed to the speak() method which does the actual speaking and works. 这被传递到speak()方法,该方法执行实际的语音操作。

The issue is with the onUtteranceProgressListener . 问题出在onUtteranceProgressListener

Sometimes the onStart(String s) method is called, sometimes it isn't! 有时会onStart(String s)方法,有时则不会! 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. 通常情况下,它会被要求随后调用speak()但并非总是如此。 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. 永远不会调用onDoneonErroronStop

I have tried using different utteranceID and Bundle values in the tts.speak() call but this makes to difference. 我曾尝试使用不同的尝试utteranceIDBundle中的值tts.speak()调用,但是这使得以区别。

When the app is started, the first status display is 'initialised' which means that the onUtteranceListener must have been set within the onInit method. 当应用程序被启动时,所述第一状态显示“初始化”,这意味着该onUtteranceListener必须已在内设置onInit方法。 The TextToSpeech object is instantiated within the activity's onCreate method. TextToSpeech对象在活动的onCreate方法中实例化。

I have looked through all the information I can find which has mostly suggested getting the utteranceID correct. 我仔细阅读了所有可以找到的信息,这些信息大多建议正确设置utteranceID What else can I try in order to get a better understanding of this problem please? 为了更好地理解这个问题,我还能尝试什么?

The problem is that the onDone() method (and in fact any of the progress callbacks) is run on a background thread, and therefore the Toast is not going to work, and any code that accesses your UI such as setText(...) may or may not work. 问题在于onDone()方法(实际上是任何进度回调)都在后台线程上运行,因此Toast无法正常工作,并且任何访问您的UI的代码(例如setText(...) )可能会或可能不会。

So... the methods probably are being called, but you just can't see that. 所以...这些方法可能正在被调用,但是您根本看不到。

The solution to this would be to surround the code in your callbacks with runOnUiThread() like this: 解决方案是使用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;
        }
    });

}

Note: It's probably best to initialize your TextView in onCreate() along with everything else instead of in the progress callbacks. 注意:最好在onCreate()中初始化TextView以及其他所有东西,而不是在进度回调中初始化。

Also, the purpose of the utteranceID is to give each call to speak() a unique identifier that is then passed back to you as the "String s" argument in the progress callbacks. 同样,utteranceID的目的是为每个对talk()的调用提供一个唯一的标识符,该标识符然后作为进度回调中的“字符串s”参数传递回给您。

It's a good idea to give each call to speak a new ("most recent") ID using some kind of random number generator, and then checking it in the progress callbacks. 一个好主意是使用某种随机数发生器为每个呼叫讲一个新的(“最近的”)ID,然后在进度回调中对其进行检查。

You can see a similar question and answer regarding this here . 您可以在此处看到类似的问题和答案。

Side note: 边注:

Since you have a "restart" button, you should know that on APIs <23, calls to TextToSpeech.stop() will cause the onDone() in your progress listener to be called. 由于您具有“重新启动”按钮,因此您应该知道,在<23的API上,对TextToSpeech.stop()的调用将导致在进度侦听器中调用onDone()。 On APIs 23+, it calls onStop() instead. 在API 23+上,它改为调用onStop()。

First, make sure you actually have a problem and not a race between who sets the text in what order. 首先,请确保您确实有问题,而不是谁在按什么顺序设置文本之间存在争执。 Use log statements to make sure it is not actually called. 使用日志语句来确保未真正调用它。

Try setting queueMode to QUEUE_ADD like: 尝试将queueMode设置为QUEUE_ADD,例如:

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

maybe subsequent calls are cancelling the listener's events from previous texts inputs, as QUEUE_FLUSH suggests. 如QUEUE_FLUSH所建议的,随后的调用可能会取消先前文本输入中的侦听器事件。

Also the bundle isn't really needed there, you can set it to null. 另外,实际上并不需要捆绑包,您可以将其设置为null。

Hope any of these helps. 希望以上任何帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM