• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1;
4 import static android.os.Build.VERSION_CODES.LOLLIPOP;
5 import static java.nio.charset.StandardCharsets.UTF_8;
6 import static org.robolectric.util.reflector.Reflector.reflector;
7 
8 import android.content.Context;
9 import android.os.Bundle;
10 import android.os.Handler;
11 import android.os.Looper;
12 import android.speech.tts.TextToSpeech;
13 import android.speech.tts.TextToSpeech.Engine;
14 import android.speech.tts.UtteranceProgressListener;
15 import android.speech.tts.Voice;
16 import com.google.common.collect.ImmutableList;
17 import java.io.File;
18 import java.io.IOException;
19 import java.io.PrintWriter;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Set;
26 import org.robolectric.RuntimeEnvironment;
27 import org.robolectric.annotation.Implementation;
28 import org.robolectric.annotation.Implements;
29 import org.robolectric.annotation.RealObject;
30 import org.robolectric.annotation.Resetter;
31 import org.robolectric.shadow.api.Shadow;
32 import org.robolectric.shadows.ShadowMediaPlayer.MediaInfo;
33 import org.robolectric.shadows.util.DataSource;
34 import org.robolectric.util.ReflectionHelpers;
35 import org.robolectric.util.ReflectionHelpers.ClassParameter;
36 import org.robolectric.util.reflector.Direct;
37 import org.robolectric.util.reflector.ForType;
38 
39 @Implements(TextToSpeech.class)
40 public class ShadowTextToSpeech {
41 
42   private static final Set<Locale> languageAvailabilities = new HashSet<>();
43   private static final Set<Voice> voices = new HashSet<>();
44   private static TextToSpeech lastTextToSpeechInstance;
45 
46   @RealObject private TextToSpeech tts;
47 
48   private Context context;
49   private TextToSpeech.OnInitListener listener;
50   private String lastSpokenText;
51   private boolean shutdown = false;
52   private boolean stopped = true;
53   private int queueMode = -1;
54   private Locale language = null;
55   private File lastSynthesizeToFile;
56   private String lastSynthesizeToFileText;
57   private Voice currentVoice = null;
58 
59   // This is not the value returned by synthesizeToFile, but rather controls the callbacks.
60   // See
61   // http://cs/android/frameworks/base/core/java/android/speech/tts/TextToSpeech.java?rcl=db6d9c1ced6b9af1de8f12e912a223f3c7f42ecd&l=1874.
62   private int synthesizeToFileResult = TextToSpeech.SUCCESS;
63 
64   private boolean completeSynthesis = false;
65 
66   private final List<String> spokenTextList = new ArrayList<>();
67 
68   @Implementation
__constructor__( Context context, TextToSpeech.OnInitListener listener, String engine, String packageName, boolean useFallback)69   protected void __constructor__(
70       Context context,
71       TextToSpeech.OnInitListener listener,
72       String engine,
73       String packageName,
74       boolean useFallback) {
75     this.context = context;
76     this.listener = listener;
77     lastTextToSpeechInstance = tts;
78     Shadow.invokeConstructor(
79         TextToSpeech.class,
80         tts,
81         ClassParameter.from(Context.class, context),
82         ClassParameter.from(TextToSpeech.OnInitListener.class, listener),
83         ClassParameter.from(String.class, engine),
84         ClassParameter.from(String.class, packageName),
85         ClassParameter.from(boolean.class, useFallback));
86   }
87 
88   /**
89    * Sets up synthesizeToFile to succeed or fail in the synthesis operation.
90    *
91    * <p>This controls calls the relevant callbacks but does not set the return value of
92    * synthesizeToFile.
93    *
94    * @param result TextToSpeech enum (SUCCESS, ERROR, or one of the ERROR_ codes from TextToSpeech)
95    */
simulateSynthesizeToFileResult(int result)96   public void simulateSynthesizeToFileResult(int result) {
97     this.synthesizeToFileResult = result;
98     this.completeSynthesis = true;
99   }
100 
101   @Implementation
initTts()102   protected int initTts() {
103     // Has to be overridden because the real code attempts to connect to a non-existent TTS
104     // system service.
105     return TextToSpeech.SUCCESS;
106   }
107 
108   /**
109    * Speaks the string using the specified queuing strategy and speech parameters.
110    *
111    * @param params The real implementation converts the hashmap into a bundle, but the bundle
112    *     argument is not used in the shadow implementation.
113    */
114   @Implementation
speak( final String text, final int queueMode, final HashMap<String, String> params)115   protected int speak(
116       final String text, final int queueMode, final HashMap<String, String> params) {
117     if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
118       return reflector(TextToSpeechReflector.class, tts).speak(text, queueMode, params);
119     }
120     return speak(
121         text, queueMode, null, params == null ? null : params.get(Engine.KEY_PARAM_UTTERANCE_ID));
122   }
123 
124   @Implementation(minSdk = LOLLIPOP)
speak( final CharSequence text, final int queueMode, final Bundle params, final String utteranceId)125   protected int speak(
126       final CharSequence text, final int queueMode, final Bundle params, final String utteranceId) {
127     stopped = false;
128     lastSpokenText = text.toString();
129     spokenTextList.add(text.toString());
130     this.queueMode = queueMode;
131 
132     if (RuntimeEnvironment.getApiLevel() >= ICE_CREAM_SANDWICH_MR1) {
133       if (utteranceId != null) {
134         // The onStart and onDone callbacks are normally delivered asynchronously. Since in
135         // Robolectric we don't need the wait for TTS package, the asynchronous callbacks are
136         // simulated by posting it on a handler. The behavior of the callback can be changed for
137         // each individual test by changing the idling mode of the foreground scheduler.
138         Handler handler = new Handler(Looper.getMainLooper());
139         handler.post(
140             () -> {
141               UtteranceProgressListener utteranceProgressListener = getUtteranceProgressListener();
142               if (utteranceProgressListener != null) {
143                 utteranceProgressListener.onStart(utteranceId);
144               }
145               // The onDone callback is posted in a separate run-loop from onStart, so that tests
146               // can pause the scheduler and test the behavior between these two callbacks.
147               handler.post(
148                   () -> {
149                     UtteranceProgressListener utteranceProgressListener2 =
150                         getUtteranceProgressListener();
151                     if (utteranceProgressListener2 != null) {
152                       utteranceProgressListener2.onDone(utteranceId);
153                     }
154                   });
155             });
156       }
157     }
158     return TextToSpeech.SUCCESS;
159   }
160 
161   @Implementation
shutdown()162   protected void shutdown() {
163     shutdown = true;
164   }
165 
166   @Implementation
stop()167   protected int stop() {
168     stopped = true;
169     return TextToSpeech.SUCCESS;
170   }
171 
172   @Implementation
isLanguageAvailable(Locale lang)173   protected int isLanguageAvailable(Locale lang) {
174     for (Locale locale : languageAvailabilities) {
175       if (locale.getISO3Language().equals(lang.getISO3Language())) {
176         if (locale.getISO3Country().equals(lang.getISO3Country())) {
177           if (locale.getVariant().equals(lang.getVariant())) {
178             return TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
179           }
180           return TextToSpeech.LANG_COUNTRY_AVAILABLE;
181         }
182         return TextToSpeech.LANG_AVAILABLE;
183       }
184     }
185     return TextToSpeech.LANG_NOT_SUPPORTED;
186   }
187 
188   @Implementation
setLanguage(Locale locale)189   protected int setLanguage(Locale locale) {
190     this.language = locale;
191     return isLanguageAvailable(locale);
192   }
193 
194   /**
195    * Stores {@code text} and returns {@link TextToSpeech#SUCCESS}.
196    *
197    * @see #getLastSynthesizeToFileText()
198    */
199   @Implementation(minSdk = LOLLIPOP)
synthesizeToFile(CharSequence text, Bundle params, File file, String utteranceId)200   protected int synthesizeToFile(CharSequence text, Bundle params, File file, String utteranceId)
201       throws IOException {
202     this.lastSynthesizeToFileText = text.toString();
203 
204     if (!Boolean.getBoolean("robolectric.enableShadowTtsSynthesisToFileWriteToFileSuppression")) {
205       this.lastSynthesizeToFile = file;
206       try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
207         writer.println(text);
208       }
209 
210       ShadowMediaPlayer.addMediaInfo(
211           DataSource.toDataSource(file.getAbsolutePath()), new MediaInfo());
212     }
213 
214     UtteranceProgressListener utteranceProgressListener = getUtteranceProgressListener();
215 
216     /*
217      * The Java system property robolectric.shadowTtsEnableSynthesisToFileCallbackSuppression can be
218      * used by test targets that fail due to tests relying on previous behavior of this fake, where
219      * the listeners were not called.
220      */
221     if (completeSynthesis
222         && utteranceProgressListener != null
223         && !Boolean.getBoolean("robolectric.enableShadowTtsSynthesisToFileCallbackSuppression")) {
224       switch (synthesizeToFileResult) {
225           // Right now this only supports success an error though there are other possible
226           // situations.
227         case TextToSpeech.SUCCESS:
228           utteranceProgressListener.onStart(utteranceId);
229           utteranceProgressListener.onDone(utteranceId);
230           break;
231         default:
232           utteranceProgressListener.onError(utteranceId, synthesizeToFileResult);
233           break;
234       }
235     }
236 
237     // This refers to the result of the queueing operation.
238     // See
239     // http://cs/android/frameworks/base/core/java/android/speech/tts/TextToSpeech.java?rcl=db6d9c1ced6b9af1de8f12e912a223f3c7f42ecd&l=1890.
240     return TextToSpeech.SUCCESS;
241   }
242 
243   @Implementation(minSdk = LOLLIPOP)
setVoice(Voice voice)244   protected int setVoice(Voice voice) {
245     this.currentVoice = voice;
246     return TextToSpeech.SUCCESS;
247   }
248 
249   @Implementation(minSdk = LOLLIPOP)
getVoices()250   protected Set<Voice> getVoices() {
251     return voices;
252   }
253 
getUtteranceProgressListener()254   public UtteranceProgressListener getUtteranceProgressListener() {
255     return ReflectionHelpers.getField(tts, "mUtteranceProgressListener");
256   }
257 
getContext()258   public Context getContext() {
259     return context;
260   }
261 
getOnInitListener()262   public TextToSpeech.OnInitListener getOnInitListener() {
263     return listener;
264   }
265 
getLastSpokenText()266   public String getLastSpokenText() {
267     return lastSpokenText;
268   }
269 
clearLastSpokenText()270   public void clearLastSpokenText() {
271     lastSpokenText = null;
272   }
273 
isShutdown()274   public boolean isShutdown() {
275     return shutdown;
276   }
277 
278   /** @return {@code true} if the TTS is stopped. */
isStopped()279   public boolean isStopped() {
280     return stopped;
281   }
282 
getQueueMode()283   public int getQueueMode() {
284     return queueMode;
285   }
286 
287   /**
288    * Returns {@link Locale} set using {@link TextToSpeech#setLanguage(Locale)} or null if not set.
289    */
getCurrentLanguage()290   public Locale getCurrentLanguage() {
291     return language;
292   }
293 
294   /**
295    * Returns last text {@link CharSequence} passed to {@link
296    * TextToSpeech#synthesizeToFile(CharSequence, Bundle, File, String)}.
297    */
getLastSynthesizeToFileText()298   public String getLastSynthesizeToFileText() {
299     return lastSynthesizeToFileText;
300   }
301 
302   /**
303    * Returns last file {@link File} written to by {@link TextToSpeech#synthesizeToFile(CharSequence,
304    * Bundle, File, String)}.
305    */
getLastSynthesizeToFile()306   public File getLastSynthesizeToFile() {
307     return lastSynthesizeToFile;
308   }
309 
310   /** Returns list of all the text spoken by {@link #speak}. */
getSpokenTextList()311   public ImmutableList<String> getSpokenTextList() {
312     return ImmutableList.copyOf(spokenTextList);
313   }
314 
315   /**
316    * Makes {@link Locale} an available language returned by {@link
317    * TextToSpeech#isLanguageAvailable(Locale)}. The value returned by {@link
318    * #isLanguageAvailable(Locale)} will vary depending on language, country, and variant.
319    */
addLanguageAvailability(Locale locale)320   public static void addLanguageAvailability(Locale locale) {
321     languageAvailabilities.add(locale);
322   }
323 
324   /** Makes {@link Voice} an available voice returned by {@link TextToSpeech#getVoices()}. */
addVoice(Voice voice)325   public static void addVoice(Voice voice) {
326     voices.add(voice);
327   }
328 
329   /** Returns {@link Voice} set using {@link TextToSpeech#setVoice(Voice)}, or null if not set. */
getCurrentVoice()330   public Voice getCurrentVoice() {
331     return currentVoice;
332   }
333 
334   /** Returns the most recently instantiated {@link TextToSpeech} or null if none exist. */
getLastTextToSpeechInstance()335   public static TextToSpeech getLastTextToSpeechInstance() {
336     return lastTextToSpeechInstance;
337   }
338 
339   @Resetter
reset()340   public static void reset() {
341     languageAvailabilities.clear();
342     voices.clear();
343     lastTextToSpeechInstance = null;
344   }
345 
346   @ForType(TextToSpeech.class)
347   interface TextToSpeechReflector {
348 
349     @Direct
speak(final String text, final int queueMode, final HashMap params)350     int speak(final String text, final int queueMode, final HashMap params);
351   }
352 }
353