All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowTextToSpeech Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.Engine;
import android.speech.tts.UtteranceProgressListener;
import android.speech.tts.Voice;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowMediaPlayer.MediaInfo;
import org.robolectric.shadows.util.DataSource;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;

@Implements(TextToSpeech.class)
public class ShadowTextToSpeech {

  private static final Set languageAvailabilities = new HashSet<>();
  private static final Set voices = new HashSet<>();
  private static TextToSpeech lastTextToSpeechInstance;

  @RealObject private TextToSpeech tts;

  private Context context;
  private TextToSpeech.OnInitListener listener;
  private String lastSpokenText;
  private boolean shutdown = false;
  private boolean stopped = true;
  private int queueMode = -1;
  private Locale language = null;
  private File lastSynthesizeToFile;
  private String lastSynthesizeToFileText;
  private Voice currentVoice = null;

  // This is not the value returned by synthesizeToFile, but rather controls the callbacks.
  // See
  // http://cs/android/frameworks/base/core/java/android/speech/tts/TextToSpeech.java?rcl=db6d9c1ced6b9af1de8f12e912a223f3c7f42ecd&l=1874.
  private int synthesizeToFileResult = TextToSpeech.SUCCESS;

  private boolean completeSynthesis = false;

  private final List spokenTextList = new ArrayList<>();

  @Implementation
  protected void __constructor__(
      Context context,
      TextToSpeech.OnInitListener listener,
      String engine,
      String packageName,
      boolean useFallback) {
    this.context = context;
    this.listener = listener;
    lastTextToSpeechInstance = tts;
    Shadow.invokeConstructor(
        TextToSpeech.class,
        tts,
        ClassParameter.from(Context.class, context),
        ClassParameter.from(TextToSpeech.OnInitListener.class, listener),
        ClassParameter.from(String.class, engine),
        ClassParameter.from(String.class, packageName),
        ClassParameter.from(boolean.class, useFallback));
  }

  /**
   * Sets up synthesizeToFile to succeed or fail in the synthesis operation.
   *
   * 

This controls calls the relevant callbacks but does not set the return value of * synthesizeToFile. * * @param result TextToSpeech enum (SUCCESS, ERROR, or one of the ERROR_ codes from TextToSpeech) */ public void simulateSynthesizeToFileResult(int result) { this.synthesizeToFileResult = result; this.completeSynthesis = true; } @Implementation protected int initTts() { // Has to be overridden because the real code attempts to connect to a non-existent TTS // system service. return TextToSpeech.SUCCESS; } /** * Speaks the string using the specified queuing strategy and speech parameters. * * @param params The real implementation converts the hashmap into a bundle, but the bundle * argument is not used in the shadow implementation. */ @Implementation protected int speak( final String text, final int queueMode, final HashMap params) { if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) { return reflector(TextToSpeechReflector.class, tts).speak(text, queueMode, params); } return speak( text, queueMode, null, params == null ? null : params.get(Engine.KEY_PARAM_UTTERANCE_ID)); } @Implementation(minSdk = LOLLIPOP) protected int speak( final CharSequence text, final int queueMode, final Bundle params, final String utteranceId) { stopped = false; lastSpokenText = text.toString(); spokenTextList.add(text.toString()); this.queueMode = queueMode; if (RuntimeEnvironment.getApiLevel() >= ICE_CREAM_SANDWICH_MR1) { if (utteranceId != null) { // The onStart and onDone callbacks are normally delivered asynchronously. Since in // Robolectric we don't need the wait for TTS package, the asynchronous callbacks are // simulated by posting it on a handler. The behavior of the callback can be changed for // each individual test by changing the idling mode of the foreground scheduler. Handler handler = new Handler(Looper.getMainLooper()); handler.post( () -> { UtteranceProgressListener utteranceProgressListener = getUtteranceProgressListener(); if (utteranceProgressListener != null) { utteranceProgressListener.onStart(utteranceId); } // The onDone callback is posted in a separate run-loop from onStart, so that tests // can pause the scheduler and test the behavior between these two callbacks. handler.post( () -> { UtteranceProgressListener utteranceProgressListener2 = getUtteranceProgressListener(); if (utteranceProgressListener2 != null) { utteranceProgressListener2.onDone(utteranceId); } }); }); } } return TextToSpeech.SUCCESS; } @Implementation protected void shutdown() { shutdown = true; } @Implementation protected int stop() { stopped = true; return TextToSpeech.SUCCESS; } @Implementation protected int isLanguageAvailable(Locale lang) { for (Locale locale : languageAvailabilities) { if (locale.getISO3Language().equals(lang.getISO3Language())) { if (locale.getISO3Country().equals(lang.getISO3Country())) { if (locale.getVariant().equals(lang.getVariant())) { return TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; } return TextToSpeech.LANG_COUNTRY_AVAILABLE; } return TextToSpeech.LANG_AVAILABLE; } } return TextToSpeech.LANG_NOT_SUPPORTED; } @Implementation protected int setLanguage(Locale locale) { this.language = locale; return isLanguageAvailable(locale); } /** * Stores {@code text} and returns {@link TextToSpeech#SUCCESS}. * * @see #getLastSynthesizeToFileText() */ @Implementation(minSdk = LOLLIPOP) protected int synthesizeToFile(CharSequence text, Bundle params, File file, String utteranceId) throws IOException { this.lastSynthesizeToFileText = text.toString(); if (!Boolean.getBoolean("robolectric.enableShadowTtsSynthesisToFileWriteToFileSuppression")) { this.lastSynthesizeToFile = file; try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) { writer.println(text); } ShadowMediaPlayer.addMediaInfo( DataSource.toDataSource(file.getAbsolutePath()), new MediaInfo()); } UtteranceProgressListener utteranceProgressListener = getUtteranceProgressListener(); /* * The Java system property robolectric.shadowTtsEnableSynthesisToFileCallbackSuppression can be * used by test targets that fail due to tests relying on previous behavior of this fake, where * the listeners were not called. */ if (completeSynthesis && utteranceProgressListener != null && !Boolean.getBoolean("robolectric.enableShadowTtsSynthesisToFileCallbackSuppression")) { switch (synthesizeToFileResult) { // Right now this only supports success an error though there are other possible // situations. case TextToSpeech.SUCCESS: utteranceProgressListener.onStart(utteranceId); utteranceProgressListener.onDone(utteranceId); break; default: utteranceProgressListener.onError(utteranceId, synthesizeToFileResult); break; } } // This refers to the result of the queueing operation. // See // http://cs/android/frameworks/base/core/java/android/speech/tts/TextToSpeech.java?rcl=db6d9c1ced6b9af1de8f12e912a223f3c7f42ecd&l=1890. return TextToSpeech.SUCCESS; } @Implementation(minSdk = LOLLIPOP) protected int setVoice(Voice voice) { this.currentVoice = voice; return TextToSpeech.SUCCESS; } @Implementation(minSdk = LOLLIPOP) protected Set getVoices() { return voices; } public UtteranceProgressListener getUtteranceProgressListener() { return ReflectionHelpers.getField(tts, "mUtteranceProgressListener"); } public Context getContext() { return context; } public TextToSpeech.OnInitListener getOnInitListener() { return listener; } public String getLastSpokenText() { return lastSpokenText; } public void clearLastSpokenText() { lastSpokenText = null; } public boolean isShutdown() { return shutdown; } /** @return {@code true} if the TTS is stopped. */ public boolean isStopped() { return stopped; } public int getQueueMode() { return queueMode; } /** * Returns {@link Locale} set using {@link TextToSpeech#setLanguage(Locale)} or null if not set. */ public Locale getCurrentLanguage() { return language; } /** * Returns last text {@link CharSequence} passed to {@link * TextToSpeech#synthesizeToFile(CharSequence, Bundle, File, String)}. */ public String getLastSynthesizeToFileText() { return lastSynthesizeToFileText; } /** * Returns last file {@link File} written to by {@link TextToSpeech#synthesizeToFile(CharSequence, * Bundle, File, String)}. */ public File getLastSynthesizeToFile() { return lastSynthesizeToFile; } /** Returns list of all the text spoken by {@link #speak}. */ public ImmutableList getSpokenTextList() { return ImmutableList.copyOf(spokenTextList); } /** * Makes {@link Locale} an available language returned by {@link * TextToSpeech#isLanguageAvailable(Locale)}. The value returned by {@link * #isLanguageAvailable(Locale)} will vary depending on language, country, and variant. */ public static void addLanguageAvailability(Locale locale) { languageAvailabilities.add(locale); } /** Makes {@link Voice} an available voice returned by {@link TextToSpeech#getVoices()}. */ public static void addVoice(Voice voice) { voices.add(voice); } /** Returns {@link Voice} set using {@link TextToSpeech#setVoice(Voice)}, or null if not set. */ public Voice getCurrentVoice() { return currentVoice; } /** Returns the most recently instantiated {@link TextToSpeech} or null if none exist. */ public static TextToSpeech getLastTextToSpeechInstance() { return lastTextToSpeechInstance; } @Resetter public static void reset() { languageAvailabilities.clear(); voices.clear(); lastTextToSpeechInstance = null; } @ForType(TextToSpeech.class) interface TextToSpeechReflector { @Direct int speak(final String text, final int queueMode, final HashMap params); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy