diff options
7 files changed, 115 insertions, 21 deletions
diff --git a/api/current.txt b/api/current.txt index 05aada4..b051768 100644 --- a/api/current.txt +++ b/api/current.txt @@ -27752,6 +27752,7 @@ package android.speech.tts { method public abstract deprecated void onError(java.lang.String); method public void onError(java.lang.String, int); method public abstract void onStart(java.lang.String); + method public void onStop(java.lang.String, boolean); } public class Voice implements android.os.Parcelable { diff --git a/core/java/android/speech/tts/ITextToSpeechCallback.aidl b/core/java/android/speech/tts/ITextToSpeechCallback.aidl index 899515f..d785c3f 100644 --- a/core/java/android/speech/tts/ITextToSpeechCallback.aidl +++ b/core/java/android/speech/tts/ITextToSpeechCallback.aidl @@ -40,7 +40,7 @@ oneway interface ITextToSpeechCallback { * * @param utteranceId Unique id identifying synthesis request. */ - void onStop(String utteranceId); + void onStop(String utteranceId, boolean isStarted); /** * Tells the client that the synthesis has failed. diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index c59ca8a..06e9ce0 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -2066,10 +2066,10 @@ public class TextToSpeech { private boolean mEstablished; private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { - public void onStop(String utteranceId) throws RemoteException { + public void onStop(String utteranceId, boolean isStarted) throws RemoteException { UtteranceProgressListener listener = mUtteranceProgressListener; if (listener != null) { - listener.onDone(utteranceId); + listener.onStop(utteranceId, isStarted); } }; diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index 9bb7f02..02c9a36 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -455,10 +455,37 @@ public abstract class TextToSpeechService extends Service { private class SynthHandler extends Handler { private SpeechItem mCurrentSpeechItem = null; + private ArrayList<Object> mFlushedObjects = new ArrayList<Object>(); + private boolean mFlushAll; + public SynthHandler(Looper looper) { super(looper); } + private void startFlushingSpeechItems(Object callerIdentity) { + synchronized (mFlushedObjects) { + if (callerIdentity == null) { + mFlushAll = true; + } else { + mFlushedObjects.add(callerIdentity); + } + } + } + private void endFlushingSpeechItems(Object callerIdentity) { + synchronized (mFlushedObjects) { + if (callerIdentity == null) { + mFlushAll = false; + } else { + mFlushedObjects.remove(callerIdentity); + } + } + } + private boolean isFlushed(SpeechItem speechItem) { + synchronized (mFlushedObjects) { + return mFlushAll || mFlushedObjects.contains(speechItem.getCallerIdentity()); + } + } + private synchronized SpeechItem getCurrentSpeechItem() { return mCurrentSpeechItem; } @@ -522,9 +549,13 @@ public abstract class TextToSpeechService extends Service { Runnable runnable = new Runnable() { @Override public void run() { - setCurrentSpeechItem(speechItem); - speechItem.play(); - setCurrentSpeechItem(null); + if (isFlushed(speechItem)) { + speechItem.stop(); + } else { + setCurrentSpeechItem(speechItem); + speechItem.play(); + setCurrentSpeechItem(null); + } } }; Message msg = Message.obtain(this, runnable); @@ -552,12 +583,14 @@ public abstract class TextToSpeechService extends Service { * * Called on a service binder thread. */ - public int stopForApp(Object callerIdentity) { + public int stopForApp(final Object callerIdentity) { if (callerIdentity == null) { return TextToSpeech.ERROR; } - removeCallbacksAndMessages(callerIdentity); + // Flush pending messages from callerIdentity + startFlushingSpeechItems(callerIdentity); + // This stops writing data to the file / or publishing // items to the audio playback handler. // @@ -573,20 +606,39 @@ public abstract class TextToSpeechService extends Service { // Remove any enqueued audio too. mAudioPlaybackHandler.stopForApp(callerIdentity); + // Stop flushing pending messages + Runnable runnable = new Runnable() { + @Override + public void run() { + endFlushingSpeechItems(callerIdentity); + } + }; + sendMessage(Message.obtain(this, runnable)); return TextToSpeech.SUCCESS; } public int stopAll() { + // Order to flush pending messages + startFlushingSpeechItems(null); + // Stop the current speech item unconditionally . SpeechItem current = setCurrentSpeechItem(null); if (current != null) { current.stop(); } - // Remove all other items from the queue. - removeCallbacksAndMessages(null); // Remove all pending playback as well. mAudioPlaybackHandler.stop(); + // Message to stop flushing pending messages + Runnable runnable = new Runnable() { + @Override + public void run() { + endFlushingSpeechItems(null); + } + }; + sendMessage(Message.obtain(this, runnable)); + + return TextToSpeech.SUCCESS; } } @@ -698,7 +750,6 @@ public abstract class TextToSpeechService extends Service { return mCallerIdentity; } - public int getCallerUid() { return mCallerUid; } @@ -752,6 +803,10 @@ public abstract class TextToSpeechService extends Service { protected synchronized boolean isStopped() { return mStopped; } + + protected synchronized boolean isStarted() { + return mStarted; + } } /** @@ -777,7 +832,7 @@ public abstract class TextToSpeechService extends Service { public void dispatchOnStop() { final String utteranceId = getUtteranceId(); if (utteranceId != null) { - mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId); + mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId, isStarted()); } } @@ -940,6 +995,8 @@ public abstract class TextToSpeechService extends Service { // turn implies that synthesis would not have started. synthesisCallback.stop(); TextToSpeechService.this.onStop(); + } else { + dispatchOnStop(); } } @@ -1345,11 +1402,11 @@ public abstract class TextToSpeechService extends Service { } } - public void dispatchOnStop(Object callerIdentity, String utteranceId) { + public void dispatchOnStop(Object callerIdentity, String utteranceId, boolean started) { ITextToSpeechCallback cb = getCallbackFor(callerIdentity); if (cb == null) return; try { - cb.onStop(utteranceId); + cb.onStop(utteranceId, started); } catch (RemoteException e) { Log.e(TAG, "Callback onStop failed: " + e); } diff --git a/core/java/android/speech/tts/UtteranceProgressListener.java b/core/java/android/speech/tts/UtteranceProgressListener.java index 6769794..9eb22ef 100644 --- a/core/java/android/speech/tts/UtteranceProgressListener.java +++ b/core/java/android/speech/tts/UtteranceProgressListener.java @@ -60,6 +60,20 @@ public abstract class UtteranceProgressListener { } /** + * Called when an utterance has been stopped while in progress or flushed from the + * synthesis queue. This can happen if client calls {@link TextToSpeech#stop()} + * or use {@link TextToSpeech#QUEUE_FLUSH} as an argument in + * {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods. + * + * @param utteranceId the utterance ID of the utterance. + * @param isStarted If true, then utterance was interrupted while being synthesized + * and it's output is incomplete. If it's false, then utterance was flushed + * before the synthesis started. + */ + public void onStop(String utteranceId, boolean isStarted) { + } + + /** * Wraps an old deprecated OnUtteranceCompletedListener with a shiny new * progress listener. * @@ -83,6 +97,11 @@ public abstract class UtteranceProgressListener { // Left unimplemented, has no equivalent in the old // API. } + + @Override + public void onStop(String utteranceId, boolean isStarted) { + listener.onUtteranceCompleted(utteranceId); + } }; } } diff --git a/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java b/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java index 20648a4..06fbcdc 100644 --- a/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java +++ b/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java @@ -19,8 +19,10 @@ package com.android.speech.tts; import android.speech.tts.SynthesisCallback; import android.speech.tts.SynthesisRequest; import android.speech.tts.TextToSpeechService; +import android.util.Log; import java.util.ArrayList; +import java.util.logging.Logger; public class MockableTextToSpeechService extends TextToSpeechService { diff --git a/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java b/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java index 78d4f96..faf6827 100644 --- a/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java +++ b/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java @@ -43,11 +43,18 @@ public class TextToSpeechTests extends InstrumentationTestCase { IDelegate passThrough = LittleMock.mock(IDelegate.class); MockableTextToSpeechService.setMocker(passThrough); + // For the default voice selection + LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(passThrough) + .onIsLanguageAvailable( + LittleMock.anyString(), LittleMock.anyString(), LittleMock.anyString()); + LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(passThrough) + .onLoadLanguage( + LittleMock.anyString(), LittleMock.anyString(), LittleMock.anyString()); + blockingInitAndVerify(MOCK_ENGINE, TextToSpeech.SUCCESS); assertEquals(MOCK_ENGINE, mTts.getCurrentEngine()); } - @Override public void tearDown() { if (mTts != null) { @@ -77,7 +84,7 @@ public class TextToSpeechTests extends InstrumentationTestCase { assertEquals(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE, mTts.setLanguage(new Locale("eng", "USA", "variant"))); LittleMock.verify(delegate, LittleMock.anyTimes()).onIsLanguageAvailable( "eng", "USA", "variant"); - LittleMock.verify(delegate, LittleMock.times(1)).onLoadLanguage( + LittleMock.verify(delegate, LittleMock.anyTimes()).onLoadLanguage( "eng", "USA", "variant"); } @@ -147,26 +154,34 @@ public class TextToSpeechTests extends InstrumentationTestCase { public void testDefaultLanguage_setsVoiceName() throws Exception { IDelegate delegate = LittleMock.mock(IDelegate.class); MockableTextToSpeechService.setMocker(delegate); + Locale defaultLocale = Locale.getDefault(); // --------------------------------------------------------- // Test that default language also sets the default voice // name - LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(delegate).onIsLanguageAvailable( - LittleMock.anyString(), LittleMock.anyString(), LittleMock.anyString()); - LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(delegate).onLoadLanguage( - LittleMock.anyString(), LittleMock.anyString(), LittleMock.anyString()); + LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE). + when(delegate).onIsLanguageAvailable( + defaultLocale.getISO3Language(), + defaultLocale.getISO3Country().toUpperCase(), + defaultLocale.getVariant()); + LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE). + when(delegate).onLoadLanguage( + defaultLocale.getISO3Language(), + defaultLocale.getISO3Country(), + defaultLocale.getVariant()); + blockingCallSpeak("foo bar", delegate); ArgumentCaptor<SynthesisRequest> req = LittleMock.createCaptor(); LittleMock.verify(delegate, LittleMock.times(1)).onSynthesizeText(req.capture(), LittleMock.<SynthesisCallback>anyObject()); - Locale defaultLocale = Locale.getDefault(); assertEquals(defaultLocale.getISO3Language(), req.getValue().getLanguage()); assertEquals(defaultLocale.getISO3Country(), req.getValue().getCountry()); assertEquals("", req.getValue().getVariant()); assertEquals(defaultLocale.toLanguageTag(), req.getValue().getVoiceName()); } + private void blockingCallSpeak(String speech, IDelegate mock) throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); |