2

On Android, activities run in the main UI thread and the TextToSpeech engine runs in a different thread. I want to update a view in an activity when the TextToSpeech engine completes playing back an utterance.

If I ignore this, then I get an android.view.ViewRoot$CalledFromWrongThreadException error when the TextToSpeech engine calls the Activity instance.

Here's my code. The error occurs on the last line of the MainActivity.java script.

TTSUser.java

package com.example.thread;

interface TTSUser {
  void ttsUtteranceComplete();
}

MainActivity.java

package com.example.thread;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity implements TTSUser {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    new TTS(this);
   }

  public void ttsUtteranceComplete() {
    TextView view_to_hide = (TextView) findViewById(R.id.hello_world);

    // On next line: android.view.ViewRoot$CalledFromWrongThreadException:
    // Only the original thread that created a view hierarchy can touch its
    // views.
    view_to_hide.setVisibility(TextView.GONE);
  }
}

TTS.java

package com.example.thread;

import android.app.Activity;
import android.content.Context;
import android.speech.tts.TextToSpeech;

import java.util.HashMap;
import java.util.Locale;

public class TTS implements TextToSpeech.OnInitListener, TextToSpeech.OnUtteranceCompletedListener {

  private final String TAG = "callback";
  private static TextToSpeech tts;
  private TTSUser activity;

  public TTS(TTSUser activity) { // Ensures access to ttsUtteranceComplete()
    this.activity = activity;
    Context context = ((Activity) activity).getApplicationContext();
    tts = new android.speech.tts.TextToSpeech(context, this);
  }

  @Override
  public void onInit(int status) {
    if (status == TextToSpeech.SUCCESS) {
      tts.setLanguage(Locale.UK);
      tts.setOnUtteranceCompletedListener(this);
      speakText("Hello World");
     }
  }

  public void speakText(String toSpeak) {
    int mode = android.speech.tts.TextToSpeech.QUEUE_FLUSH;
   // Create an id for this utterance, so that we can call back when it's done
    HashMap<String, String> hashMap = new HashMap<String, String>();
    hashMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, TAG);

    tts.speak(toSpeak, mode, hashMap);
  }

  public void onUtteranceCompleted(String utteranceID) {
    if (utteranceID.equals(TAG)) {
      activity.ttsUtteranceComplete();
    }
  }
}

I also added a line to the TextView definition in activity_main.xml, so that the Hello World text can be identified.

    android:id="@+id/hello_world"

Other answers to similarly-worded questions assume that the other thread is created explicitly in the code. Here, the thread for the TextToSpeech engine is created implicitly. How can I change my code so that the last line of MainActivity.java does not throw an error?

James Newton
  • 6,623
  • 8
  • 49
  • 113

4 Answers4

2

To execute something on the UI thread when outside of it, you can use the method runOnUiThread(Runnable) that belongs to Activity.

So you could do:

activity.runOnUiThread(new Runnable() { // EDIT: ...Ui... requires a lowercase "i"
  @Override 
  public final void run(){
     // this runs on UI thread
     activity.ttsUtteranceComplete(); // this function will run on the UI thread
  }
});
Mark Ch
  • 2,840
  • 1
  • 19
  • 31
rupps
  • 9,712
  • 4
  • 55
  • 95
  • In Android Studio, I get the warning `Cannot resolve method 'runOnUiThread(java.lang.runnable)'`. If I attempt to compile the code, I get `Error:(43, 15) error: cannot find symbol method runOnUIThread()`. Is this because of the way `activity` is obtained in the `TTS` constructor? Or do I need to import another class? – James Newton Nov 03 '14 at 03:09
  • humm... looks like what you call "activity" is another thing of type `TTSUser` ... try to cast it to `Activity` to see if it derives from a real `Activity`, else you'd have to obtain the real activity. To cast it do: `((Activity)activity).runOnUIThread .... ` – rupps Nov 03 '14 at 03:58
  • Yes, I tried that before I posted my first comment, but nothing changes. `activity` is an instance of `MainActivity` which extends `Activity` and implements `TTUSer`. Using `((Activity) activity).getApplicationContext();` works fine, but `((Activity) activity).runOnUIThread(...)` refuses to resolve. – James Newton Nov 03 '14 at 04:17
  • 1
    That's strange, runOnUiThread is a standard activity method, nothing obscure, look at the docs: http://developer.android.com/reference/android/app/Activity.html#runOnUiThread(java.lang.Runnable) ... Maybe there's a misspelling? I just noticed it's runOnUiThread with a lowercase "i". – rupps Nov 03 '14 at 04:21
  • Misspelling it is! `runOnUiThread()` with a lowercase "i" works. It is interesting that all the answers used the same misspelling. It's working for me now. And yes: method prediction is working in Android Studio. I was relying on copy-and-paste. – James Newton Nov 03 '14 at 04:25
  • haha glad to hear.. some methods are tricky to remember. However, Android Studio should have **code completion** turned on, so by just typing "activity.r" a popup window should let you choose runOnUiThread. If this is not your case, find a way to activate this functionality, it is a must! – rupps Nov 03 '14 at 04:28
1

Any updates to the user interface have to happen within the UI thread, because that is where the graphics context is handled. Your callback method therefore must also occur within the UI thread.

With a reference to the running Activity, you can call the runOnUIThread(Runnable r) method, which will synchronize your call within the graphics context. For this specific application, the following implementation could be used:

public void onUtteranceCompleted(String utteranceID) {
  if (utteranceID.equals(TAG)) {
    activity.runOnUIThread(new Runnable() {
      @Override
      public final void run() {
        activity.ttsUtteranceComplete(); 
      } 
    });
  }
}

To clean up your code, it could be worth making the runOnUIThread() call an element of the ttsUtteranceComplete() method. This would ensure that every call would be synchronized.

moffeltje
  • 4,521
  • 4
  • 33
  • 57
Mapsy
  • 4,192
  • 1
  • 37
  • 43
0

Use Activity.runOnUIThread(Runnable) - in your case it will be activity.runOnUIThread( new Runnable() { // call a method in your main activity here to update UI element});

VJ Vélan Solutions
  • 6,434
  • 5
  • 49
  • 63
0

Use a Handler on the UI thread of your Activity and then post a message to it from your background thread:

https://developer.android.com/training/multiple-threads/communicate-ui.html#Handler

Dimitar Darazhanski
  • 2,188
  • 20
  • 22