Skip to content

Commit a65c62a

Browse files
committed
Fix two TTS bugs.
(a) Fix a null pointer exception, caused by a race condition between stop / start calls. (b) Fix a deadlock observed when multiple apps call stop() when an item from one of those apps is currently being processed. bug:5253061 Change-Id: I78533aecfda028588ce6aedb041009bc0a6f4620
1 parent 38aac04 commit a65c62a

File tree

1 file changed

+56
-16
lines changed

1 file changed

+56
-16
lines changed

core/java/android/speech/tts/TextToSpeechService.java

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@ private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
260260
return old;
261261
}
262262

263+
private synchronized SpeechItem maybeRemoveCurrentSpeechItem(String callingApp) {
264+
if (mCurrentSpeechItem != null &&
265+
TextUtils.equals(mCurrentSpeechItem.getCallingApp(), callingApp)) {
266+
SpeechItem current = mCurrentSpeechItem;
267+
mCurrentSpeechItem = null;
268+
return current;
269+
}
270+
271+
return null;
272+
}
273+
263274
public boolean isSpeaking() {
264275
return getCurrentSpeechItem() != null;
265276
}
@@ -287,14 +298,9 @@ public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
287298
}
288299

289300
if (queueMode == TextToSpeech.QUEUE_FLUSH) {
290-
stop(speechItem.getCallingApp());
301+
stopForApp(speechItem.getCallingApp());
291302
} else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
292-
// Stop the current speech item.
293-
stop(speechItem.getCallingApp());
294-
// Remove all other items from the queue.
295-
removeCallbacksAndMessages(null);
296-
// Remove all pending playback as well.
297-
mAudioPlaybackHandler.removeAllItems();
303+
stopAll();
298304
}
299305
Runnable runnable = new Runnable() {
300306
@Override
@@ -305,7 +311,8 @@ public void run() {
305311
}
306312
};
307313
Message msg = Message.obtain(this, runnable);
308-
// The obj is used to remove all callbacks from the given app in stop(String).
314+
// The obj is used to remove all callbacks from the given app in
315+
// stopForApp(String).
309316
//
310317
// Note that this string is interned, so the == comparison works.
311318
msg.obj = speechItem.getCallingApp();
@@ -323,16 +330,21 @@ public void run() {
323330
*
324331
* Called on a service binder thread.
325332
*/
326-
public int stop(String callingApp) {
333+
public int stopForApp(String callingApp) {
327334
if (TextUtils.isEmpty(callingApp)) {
328335
return TextToSpeech.ERROR;
329336
}
330337

331338
removeCallbacksAndMessages(callingApp);
332339
// This stops writing data to the file / or publishing
333340
// items to the audio playback handler.
334-
SpeechItem current = setCurrentSpeechItem(null);
335-
if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) {
341+
//
342+
// Note that the current speech item must be removed only if it
343+
// belongs to the callingApp, else the item will be "orphaned" and
344+
// not stopped correctly if a stop request comes along for the item
345+
// from the app it belongs to.
346+
SpeechItem current = maybeRemoveCurrentSpeechItem(callingApp);
347+
if (current != null) {
336348
current.stop();
337349
}
338350

@@ -341,6 +353,20 @@ public int stop(String callingApp) {
341353

342354
return TextToSpeech.SUCCESS;
343355
}
356+
357+
public int stopAll() {
358+
// Stop the current speech item unconditionally.
359+
SpeechItem current = setCurrentSpeechItem(null);
360+
if (current != null) {
361+
current.stop();
362+
}
363+
// Remove all other items from the queue.
364+
removeCallbacksAndMessages(null);
365+
// Remove all pending playback as well.
366+
mAudioPlaybackHandler.removeAllItems();
367+
368+
return TextToSpeech.SUCCESS;
369+
}
344370
}
345371

346372
interface UtteranceCompletedDispatcher {
@@ -412,6 +438,10 @@ public void dispatchUtteranceCompleted() {
412438
}
413439
}
414440

441+
protected synchronized boolean isStopped() {
442+
return mStopped;
443+
}
444+
415445
protected abstract int playImpl();
416446

417447
protected abstract void stopImpl();
@@ -473,7 +503,7 @@ public boolean isValid() {
473503
Log.w(TAG, "Got empty text");
474504
return false;
475505
}
476-
if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){
506+
if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH) {
477507
Log.w(TAG, "Text too long: " + mText.length() + " chars");
478508
return false;
479509
}
@@ -485,6 +515,11 @@ protected int playImpl() {
485515
AbstractSynthesisCallback synthesisCallback;
486516
mEventLogger.onRequestProcessingStart();
487517
synchronized (this) {
518+
// stop() might have been called before we enter this
519+
// synchronized block.
520+
if (isStopped()) {
521+
return TextToSpeech.ERROR;
522+
}
488523
mSynthesisCallback = createSynthesisCallback();
489524
synthesisCallback = mSynthesisCallback;
490525
}
@@ -510,8 +545,13 @@ protected void stopImpl() {
510545
synchronized (this) {
511546
synthesisCallback = mSynthesisCallback;
512547
}
513-
synthesisCallback.stop();
514-
TextToSpeechService.this.onStop();
548+
if (synthesisCallback != null) {
549+
// If the synthesis callback is null, it implies that we haven't
550+
// entered the synchronized(this) block in playImpl which in
551+
// turn implies that synthesis would not have started.
552+
synthesisCallback.stop();
553+
TextToSpeechService.this.onStop();
554+
}
515555
}
516556

517557
public String getLanguage() {
@@ -719,7 +759,7 @@ public int stop(String callingApp) {
719759
return TextToSpeech.ERROR;
720760
}
721761

722-
return mSynthHandler.stop(intern(callingApp));
762+
return mSynthHandler.stopForApp(intern(callingApp));
723763
}
724764

725765
public String[] getLanguage() {
@@ -811,7 +851,7 @@ public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
811851
synchronized (mAppToCallback) {
812852
mAppToCallback.remove(packageName);
813853
}
814-
mSynthHandler.stop(packageName);
854+
mSynthHandler.stopForApp(packageName);
815855
}
816856

817857
@Override

0 commit comments

Comments
 (0)