// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser; import android.content.Context; import android.os.Build; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import org.chromium.base.ThreadUtils; import org.chromium.base.TraceEvent; import org.chromium.base.annotations.CalledByNative; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; /** * This class is the Java counterpart to the C++ TtsPlatformImplAndroid class. * It implements the Android-native text-to-speech code to support the web * speech synthesis API. * * Threading model note: all calls from C++ must happen on the UI thread. * Callbacks from Android may happen on a different thread, so we always * use ThreadUtils.runOnUiThread when calling back to C++. */ class TtsPlatformImpl { private static class TtsVoice { private TtsVoice(String name, String language) { mName = name; mLanguage = language; } private final String mName; private final String mLanguage; } private static class PendingUtterance { private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, String lang, float rate, float pitch, float volume) { mImpl = impl; mUtteranceId = utteranceId; mText = text; mLang = lang; mRate = rate; mPitch = pitch; mVolume = volume; } private void speak() { mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume); } TtsPlatformImpl mImpl; int mUtteranceId; String mText; String mLang; float mRate; float mPitch; float mVolume; } private long mNativeTtsPlatformImplAndroid; protected final TextToSpeech mTextToSpeech; private boolean mInitialized; private ArrayList<TtsVoice> mVoices; private String mCurrentLanguage; private PendingUtterance mPendingUtterance; protected TtsPlatformImpl(long nativeTtsPlatformImplAndroid, Context context) { mInitialized = false; mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid; mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { initialize(); } }); } } }); addOnUtteranceProgressListener(); } /** * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid * on the C++ side. * * @param nativeTtsPlatformImplAndroid The C++ object that owns us. * @param context The app context. */ @CalledByNative private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid, Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return new LollipopTtsPlatformImpl(nativeTtsPlatformImplAndroid, context); } else { return new TtsPlatformImpl(nativeTtsPlatformImplAndroid, context); } } /** * Called when our C++ counterpoint is deleted. Clear the handle to our * native C++ object, ensuring it's never called. */ @CalledByNative private void destroy() { mNativeTtsPlatformImplAndroid = 0; } /** * @return true if our TextToSpeech object is initialized and we've * finished scanning the list of voices. */ @CalledByNative private boolean isInitialized() { return mInitialized; } /** * @return the number of voices. */ @CalledByNative private int getVoiceCount() { assert mInitialized; return mVoices.size(); } /** * @return the name of the voice at a given index. */ @CalledByNative private String getVoiceName(int voiceIndex) { assert mInitialized; return mVoices.get(voiceIndex).mName; } /** * @return the language of the voice at a given index. */ @CalledByNative private String getVoiceLanguage(int voiceIndex) { assert mInitialized; return mVoices.get(voiceIndex).mLanguage; } /** * Attempt to start speaking an utterance. If it returns true, will call back on * start and end. * * @param utteranceId A unique id for this utterance so that callbacks can be tied * to a particular utterance. * @param text The text to speak. * @param lang The language code for the text (e.g., "en-US"). * @param rate The speech rate, in the units expected by Android TextToSpeech. * @param pitch The speech pitch, in the units expected by Android TextToSpeech. * @param volume The speech volume, in the units expected by Android TextToSpeech. * @return true on success. */ @CalledByNative private boolean speak(int utteranceId, String text, String lang, float rate, float pitch, float volume) { if (!mInitialized) { mPendingUtterance = new PendingUtterance(this, utteranceId, text, lang, rate, pitch, volume); return true; } if (mPendingUtterance != null) mPendingUtterance = null; if (!lang.equals(mCurrentLanguage)) { mTextToSpeech.setLanguage(new Locale(lang)); mCurrentLanguage = lang; } mTextToSpeech.setSpeechRate(rate); mTextToSpeech.setPitch(pitch); int result = callSpeak(text, volume, utteranceId); return (result == TextToSpeech.SUCCESS); } /** * Stop the current utterance. */ @CalledByNative private void stop() { if (mInitialized) mTextToSpeech.stop(); if (mPendingUtterance != null) mPendingUtterance = null; } /** * Post a task to the UI thread to send the TTS "end" event. */ protected void sendEndEventOnUiThread(final String utteranceId) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { if (mNativeTtsPlatformImplAndroid != 0) { nativeOnEndEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); } } }); } /** * Post a task to the UI thread to send the TTS "error" event. */ protected void sendErrorEventOnUiThread(final String utteranceId) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { if (mNativeTtsPlatformImplAndroid != 0) { nativeOnErrorEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); } } }); } /** * Post a task to the UI thread to send the TTS "start" event. */ protected void sendStartEventOnUiThread(final String utteranceId) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { if (mNativeTtsPlatformImplAndroid != 0) { nativeOnStartEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); } } }); } /** * This is overridden by LollipopTtsPlatformImpl because the API changed. */ @SuppressWarnings("deprecation") protected void addOnUtteranceProgressListener() { mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onDone(final String utteranceId) { sendEndEventOnUiThread(utteranceId); } // This is deprecated in Lollipop and higher but we still need to catch it // on pre-Lollipop builds. @Override @SuppressWarnings("deprecation") public void onError(final String utteranceId) { sendErrorEventOnUiThread(utteranceId); } @Override public void onStart(final String utteranceId) { sendStartEventOnUiThread(utteranceId); } }); } /** * This is overridden by LollipopTtsPlatformImpl because the API changed. */ @SuppressWarnings("deprecation") protected int callSpeak(String text, float volume, int utteranceId) { HashMap<String, String> params = new HashMap<String, String>(); if (volume != 1.0) { params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume)); } params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(utteranceId)); return mTextToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params); } /** * Note: we enforce that this method is called on the UI thread, so * we can call nativeVoicesChanged directly. */ private void initialize() { assert mNativeTtsPlatformImplAndroid != 0; TraceEvent.begin("TtsPlatformImpl:initialize"); // Note: Android supports multiple speech engines, but querying the // metadata about all of them is expensive. So we deliberately only // support the default speech engine, and expose the different // supported languages for the default engine as different voices. String defaultEngineName = mTextToSpeech.getDefaultEngine(); String engineLabel = defaultEngineName; for (TextToSpeech.EngineInfo info : mTextToSpeech.getEngines()) { if (info.name.equals(defaultEngineName)) engineLabel = info.label; } Locale[] locales = Locale.getAvailableLocales(); mVoices = new ArrayList<TtsVoice>(); for (int i = 0; i < locales.length; ++i) { if (!locales[i].getVariant().isEmpty()) continue; try { if (mTextToSpeech.isLanguageAvailable(locales[i]) > 0) { String name = locales[i].getDisplayLanguage(); if (!locales[i].getCountry().isEmpty()) { name += " " + locales[i].getDisplayCountry(); } TtsVoice voice = new TtsVoice(name, locales[i].toString()); mVoices.add(voice); } } catch (java.util.MissingResourceException e) { // Just skip the locale if it's invalid. } } mInitialized = true; nativeVoicesChanged(mNativeTtsPlatformImplAndroid); if (mPendingUtterance != null) mPendingUtterance.speak(); TraceEvent.end("TtsPlatformImpl:initialize"); } private native void nativeVoicesChanged(long nativeTtsPlatformImplAndroid); private native void nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId); private native void nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId); private native void nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId); }