34

A while ago, I discovered that playEarcon() never produces onUtteranceCompleted().

At the time I just interpreted the documentation that said "Called when an utterance has been synthesized" as onUtteranceCompleted() being not applicable for earcons because, an earcon isn't really a result of TTS synthesization.

But looking again Android's source code, I simply can't find an explanation that would justify my interpretation.

A few facts about my test jig:

  1. onUtteranceCompleted() always arrives for utterance ID preceding the earcon. That utterance is an ordinary TTS utterance, not an earcon.
  2. The earcon after that does play out (i.e. exactly as intended).
  3. onUtteranceCompleted() for that earcon never shows up. This is very consistent and reproducible behavior.

Delving deep into the TtsService source code, there seem to be only 2 methods that could affect the arrival (or absence) of onUtteranceCompleted():

  1. TtsService.processSpeechQueue()
  2. TtsService.onCompletion()

If you examine that code, you will see that a 3rd candidate, TtsService.getSoundResource() is ruled out (as being responsible for the lack of onUtteranceComplete for my earcon) because of fact #2 above: The earcon always plays, hence getSoundResource() cannot possibly return null.

Using the same logic, the 1st candidate, TtsService.processSpeechQueue(), can also be ruled out, for the same fact #2: The earcon always plays, hence the following 2 critical statements are always executed:

1108   mPlayer.setOnCompletionListener(this);
...
1111   mPlayer.start();

So, we are left with the 2nd candidate only, TtsService.onCompletion(), as a possible explanation for why a playEarcon() never produces onUtteranceCompleted():

public void onCompletion(MediaPlayer arg0) {
  // mCurrentSpeechItem may become null if it is stopped at the same
  // time it completes.
  SpeechItem currentSpeechItemCopy = mCurrentSpeechItem;
  if (currentSpeechItemCopy != null) {
    String callingApp = currentSpeechItemCopy.mCallingApp;
    ArrayList<String> params = currentSpeechItemCopy.mParams;
    String utteranceId = "";
    if (params != null) {
      for (int i = 0; i < params.size() - 1; i = i + 2) {
        String param = params.get(i);
        if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)) {
          utteranceId = params.get(i + 1);
        }
      }
    }
    if (utteranceId.length() > 0) {
      dispatchUtteranceCompletedCallback(utteranceId, callingApp);
    }
  }
  processSpeechQueue();
}

In there, there are only 2 conditions that would fail to produce dispatchUtteranceCompletedCallback():

  1. currentSpeechItemCopy == null
  2. utteranceId.length() == 0

But I know for sure that condition #2 can be ruled out because I log all utteranceIds and the earcon's are definitely there.

Also, examining the entire system log:

Log.v(SERVICE_TAG, "TTS callback: dispatch started");

The missing onUtteranceCompleted() could be the result of dispatchUtteranceCompletedCallback() not being called, but it could also be the result of mCallbacksMap.get(packageName) returning null.

So, we are left again with 2 possibilities, both of which don't make to me much sense:

  1. By the time an earcon's onCompletion() is called, earcon's mCurrentSpeechItem is null. But why?
  2. mCallbacksMap is empty. What is it and when does it ever get populated?

Any suggestions or other explanations for solving this mystery?

Community
  • 1
  • 1
an00b
  • 11,338
  • 13
  • 64
  • 101
  • 1
    We have the same problem, the workaround is to add an empty utterance after the earcon. It also looks like newer API versions (16) *do* produce the callback for earcons, looking into this. – escape-llc Jul 16 '12 at 15:09
  • 2
    There is also a nasty race condition in the `TextToSpeech` contstructor that causes your `onInit` handler to access NULL (about 1% chance) for the TTS engine, since it doesn't come in the callback parameters! It actually calls your `onInit` before the constructor finishes executing. This is very serious, because they expect you to do the initialization (`setOnUtteranceComplete`, `addEarcon`) in `onInit`. – escape-llc Jul 16 '12 at 15:17
  • 1
    Checked in emulators and `playEarcon` now sends a callback when it is done playing. This is on API 15 and higher. You can use the workaround i mentioned in previous comment for lower APIs. – escape-llc Jul 17 '12 at 10:37
  • @escape-llc Thanks++. This is exactly the workaround I've bee using for APIs lower than 15, but the fact that I don't understand why this is happening drives me crazy. Any ideas? – an00b Jul 18 '12 at 21:54
  • btw your analysis is good, thanks for taking the time. but notice that the `Map` that gets passed in the original `TextToSpeech` call is nowhere to be found in that `onCompletion` handler; it is transformed to an `ArrayList` somewhere beforehand, perhaps the defect is in that code? Perhaps `playEarcon` doesn't propagate that? Also that service talks to an `Engine` service, so that may also be a place where something doesn't propagate, I have not examined the implementation in any detail. Look at whatever sets up `mCurrentSpeechItem`. – escape-llc Jul 19 '12 at 10:18

1 Answers1

2

Check android.speech.tts.TextToSpeech#playEarcon() at line 807. The params argument passed to the text-to-speech service binder is null, which means the service never receives your utterance ID.

 public int playEarcon(String earcon, int queueMode,
         HashMap<String,String> params) {
     synchronized (mStartLock) {
         ...
         result = mITts.playEarcon(mPackageName, earcon, queueMode, null);
     }
     ...
 }
alanv
  • 23,966
  • 4
  • 93
  • 80