1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.chrome.browser; 6 7import android.content.Context; 8import android.speech.tts.TextToSpeech; 9import android.speech.tts.UtteranceProgressListener; 10 11import org.chromium.base.CalledByNative; 12import org.chromium.base.ThreadUtils; 13 14import java.util.ArrayList; 15import java.util.HashMap; 16import java.util.Locale; 17 18/** 19 * This class is the Java counterpart to the C++ TtsPlatformImplAndroid class. 20 * It implements the Android-native text-to-speech code to support the web 21 * speech synthesis API. 22 * 23 * Threading model note: all calls from C++ must happen on the UI thread. 24 * Callbacks from Android may happen on a different thread, so we always 25 * use ThreadUtils.runOnUiThread when calling back to C++. 26 */ 27class TtsPlatformImpl { 28 private static class TtsVoice { 29 private TtsVoice(String name, String language) { 30 mName = name; 31 mLanguage = language; 32 } 33 private final String mName; 34 private final String mLanguage; 35 } 36 37 private static class PendingUtterance { 38 private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, 39 String lang, float rate, float pitch, float volume) { 40 mImpl = impl; 41 mUtteranceId = utteranceId; 42 mText = text; 43 mLang = lang; 44 mRate = rate; 45 mPitch = pitch; 46 mVolume = volume; 47 } 48 49 private void speak() { 50 mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume); 51 } 52 53 TtsPlatformImpl mImpl; 54 int mUtteranceId; 55 String mText; 56 String mLang; 57 float mRate; 58 float mPitch; 59 float mVolume; 60 } 61 62 private long mNativeTtsPlatformImplAndroid; 63 private final TextToSpeech mTextToSpeech; 64 private boolean mInitialized; 65 private ArrayList<TtsVoice> mVoices; 66 private String mCurrentLanguage; 67 private PendingUtterance mPendingUtterance; 68 69 private TtsPlatformImpl(long nativeTtsPlatformImplAndroid, Context context) { 70 mInitialized = false; 71 mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid; 72 mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() { 73 @Override 74 public void onInit(int status) { 75 if (status == TextToSpeech.SUCCESS) { 76 ThreadUtils.runOnUiThread(new Runnable() { 77 @Override 78 public void run() { 79 initialize(); 80 } 81 }); 82 } 83 } 84 }); 85 mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { 86 @Override 87 public void onDone(final String utteranceId) { 88 ThreadUtils.runOnUiThread(new Runnable() { 89 @Override 90 public void run() { 91 if (mNativeTtsPlatformImplAndroid != 0) { 92 nativeOnEndEvent(mNativeTtsPlatformImplAndroid, 93 Integer.parseInt(utteranceId)); 94 } 95 } 96 }); 97 } 98 99 @Override 100 public void onError(final String utteranceId) { 101 ThreadUtils.runOnUiThread(new Runnable() { 102 @Override 103 public void run() { 104 if (mNativeTtsPlatformImplAndroid != 0) { 105 nativeOnErrorEvent(mNativeTtsPlatformImplAndroid, 106 Integer.parseInt(utteranceId)); 107 } 108 } 109 }); 110 } 111 112 @Override 113 public void onStart(final String utteranceId) { 114 ThreadUtils.runOnUiThread(new Runnable() { 115 @Override 116 public void run() { 117 if (mNativeTtsPlatformImplAndroid != 0) { 118 nativeOnStartEvent(mNativeTtsPlatformImplAndroid, 119 Integer.parseInt(utteranceId)); 120 } 121 } 122 }); 123 } 124 }); 125 } 126 127 /** 128 * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid 129 * on the C++ side. 130 * 131 * @param nativeTtsPlatformImplAndroid The C++ object that owns us. 132 * @param context The app context. 133 */ 134 @CalledByNative 135 private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid, 136 Context context) { 137 return new TtsPlatformImpl(nativeTtsPlatformImplAndroid, context); 138 } 139 140 /** 141 * Called when our C++ counterpoint is deleted. Clear the handle to our 142 * native C++ object, ensuring it's never called. 143 */ 144 @CalledByNative 145 private void destroy() { 146 mNativeTtsPlatformImplAndroid = 0; 147 } 148 149 /** 150 * @return true if our TextToSpeech object is initialized and we've 151 * finished scanning the list of voices. 152 */ 153 @CalledByNative 154 private boolean isInitialized() { 155 return mInitialized; 156 } 157 158 /** 159 * @return the number of voices. 160 */ 161 @CalledByNative 162 private int getVoiceCount() { 163 assert mInitialized == true; 164 return mVoices.size(); 165 } 166 167 /** 168 * @return the name of the voice at a given index. 169 */ 170 @CalledByNative 171 private String getVoiceName(int voiceIndex) { 172 assert mInitialized == true; 173 return mVoices.get(voiceIndex).mName; 174 } 175 176 /** 177 * @return the language of the voice at a given index. 178 */ 179 @CalledByNative 180 private String getVoiceLanguage(int voiceIndex) { 181 assert mInitialized == true; 182 return mVoices.get(voiceIndex).mLanguage; 183 } 184 185 /** 186 * Attempt to start speaking an utterance. If it returns true, will call back on 187 * start and end. 188 * 189 * @param utteranceId A unique id for this utterance so that callbacks can be tied 190 * to a particular utterance. 191 * @param text The text to speak. 192 * @param lang The language code for the text (e.g., "en-US"). 193 * @param rate The speech rate, in the units expected by Android TextToSpeech. 194 * @param pitch The speech pitch, in the units expected by Android TextToSpeech. 195 * @param volume The speech volume, in the units expected by Android TextToSpeech. 196 * @return true on success. 197 */ 198 @CalledByNative 199 private boolean speak(int utteranceId, String text, String lang, 200 float rate, float pitch, float volume) { 201 if (!mInitialized) { 202 mPendingUtterance = new PendingUtterance(this, utteranceId, text, lang, rate, 203 pitch, volume); 204 return true; 205 } 206 if (mPendingUtterance != null) mPendingUtterance = null; 207 208 if (!lang.equals(mCurrentLanguage)) { 209 mTextToSpeech.setLanguage(new Locale(lang)); 210 mCurrentLanguage = lang; 211 } 212 213 mTextToSpeech.setSpeechRate(rate); 214 mTextToSpeech.setPitch(pitch); 215 HashMap<String, String> params = new HashMap<String, String>(); 216 if (volume != 1.0) { 217 params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume)); 218 } 219 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(utteranceId)); 220 int result = mTextToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params); 221 return (result == TextToSpeech.SUCCESS); 222 } 223 224 /** 225 * Stop the current utterance. 226 */ 227 @CalledByNative 228 private void stop() { 229 if (mInitialized) mTextToSpeech.stop(); 230 if (mPendingUtterance != null) mPendingUtterance = null; 231 } 232 233 /** 234 * Note: we enforce that this method is called on the UI thread, so 235 * we can call nativeVoicesChanged directly. 236 */ 237 private void initialize() { 238 assert mNativeTtsPlatformImplAndroid != 0; 239 240 // Note: Android supports multiple speech engines, but querying the 241 // metadata about all of them is expensive. So we deliberately only 242 // support the default speech engine, and expose the different 243 // supported languages for the default engine as different voices. 244 String defaultEngineName = mTextToSpeech.getDefaultEngine(); 245 String engineLabel = defaultEngineName; 246 for (TextToSpeech.EngineInfo info : mTextToSpeech.getEngines()) { 247 if (info.name.equals(defaultEngineName)) engineLabel = info.label; 248 } 249 Locale[] locales = Locale.getAvailableLocales(); 250 mVoices = new ArrayList<TtsVoice>(); 251 for (int i = 0; i < locales.length; ++i) { 252 if (!locales[i].getVariant().isEmpty()) continue; 253 try { 254 if (mTextToSpeech.isLanguageAvailable(locales[i]) > 0) { 255 String name = locales[i].getDisplayLanguage(); 256 if (!locales[i].getCountry().isEmpty()) { 257 name += " " + locales[i].getDisplayCountry(); 258 } 259 TtsVoice voice = new TtsVoice(name, locales[i].toString()); 260 mVoices.add(voice); 261 } 262 } catch (java.util.MissingResourceException e) { 263 // Just skip the locale if it's invalid. 264 } 265 } 266 267 mInitialized = true; 268 nativeVoicesChanged(mNativeTtsPlatformImplAndroid); 269 270 if (mPendingUtterance != null) mPendingUtterance.speak(); 271 } 272 273 private native void nativeVoicesChanged(long nativeTtsPlatformImplAndroid); 274 private native void nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId); 275 private native void nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId); 276 private native void nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId); 277} 278