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 5 package org.chromium.content.browser; 6 7 import android.os.Bundle; 8 import android.speech.tts.TextToSpeech; 9 import android.speech.tts.UtteranceProgressListener; 10 11 import org.chromium.base.ContextUtils; 12 import org.chromium.base.TraceEvent; 13 import org.chromium.base.annotations.CalledByNative; 14 import org.chromium.base.annotations.JNINamespace; 15 import org.chromium.base.annotations.NativeMethods; 16 import org.chromium.base.task.AsyncTask; 17 import org.chromium.base.task.PostTask; 18 import org.chromium.content_public.browser.UiThreadTaskTraits; 19 20 import java.util.ArrayList; 21 import java.util.List; 22 import java.util.Locale; 23 24 /** 25 * This class is the Java counterpart to the C++ TtsPlatformImplAndroid class. 26 * It implements the Android-native text-to-speech code to support the web 27 * speech synthesis API. 28 * 29 * Threading model note: all calls from C++ must happen on the UI thread. 30 * Callbacks from Android may happen on a different thread, so we always 31 * use PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, ...) when calling back to C++. 32 */ 33 @JNINamespace("content") 34 class TtsPlatformImpl { 35 private static class TtsVoice { TtsVoice(String name, String language)36 private TtsVoice(String name, String language) { 37 mName = name; 38 mLanguage = language; 39 } 40 private final String mName; 41 private final String mLanguage; 42 } 43 44 private static class PendingUtterance { PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, String lang, float rate, float pitch, float volume)45 private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, String lang, 46 float rate, float pitch, float volume) { 47 mImpl = impl; 48 mUtteranceId = utteranceId; 49 mText = text; 50 mLang = lang; 51 mRate = rate; 52 mPitch = pitch; 53 mVolume = volume; 54 } 55 speak()56 private void speak() { 57 mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume); 58 } 59 60 TtsPlatformImpl mImpl; 61 int mUtteranceId; 62 String mText; 63 String mLang; 64 float mRate; 65 float mPitch; 66 float mVolume; 67 } 68 69 private long mNativeTtsPlatformImplAndroid; 70 private final TextToSpeech mTextToSpeech; 71 private boolean mInitialized; 72 private List<TtsVoice> mVoices; 73 private String mCurrentLanguage; 74 private PendingUtterance mPendingUtterance; 75 TtsPlatformImpl(long nativeTtsPlatformImplAndroid)76 private TtsPlatformImpl(long nativeTtsPlatformImplAndroid) { 77 mInitialized = false; 78 mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid; 79 mTextToSpeech = new TextToSpeech(ContextUtils.getApplicationContext(), status -> { 80 if (status == TextToSpeech.SUCCESS) { 81 PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> initialize()); 82 } 83 }); 84 addOnUtteranceProgressListener(); 85 } 86 87 /** 88 * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid 89 * on the C++ side. 90 * @param nativeTtsPlatformImplAndroid The C++ object that owns us. 91 * 92 */ 93 @CalledByNative create(long nativeTtsPlatformImplAndroid)94 private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid) { 95 return new TtsPlatformImpl(nativeTtsPlatformImplAndroid); 96 } 97 98 /** 99 * Called when our C++ counterpoint is deleted. Clear the handle to our 100 * native C++ object, ensuring it's never called. 101 */ 102 @CalledByNative destroy()103 private void destroy() { 104 mNativeTtsPlatformImplAndroid = 0; 105 } 106 107 /** 108 * @return true if our TextToSpeech object is initialized and we've 109 * finished scanning the list of voices. 110 */ 111 @CalledByNative isInitialized()112 private boolean isInitialized() { 113 return mInitialized; 114 } 115 116 /** 117 * @return the number of voices. 118 */ 119 @CalledByNative getVoiceCount()120 private int getVoiceCount() { 121 assert mInitialized; 122 return mVoices.size(); 123 } 124 125 /** 126 * @return the name of the voice at a given index. 127 */ 128 @CalledByNative getVoiceName(int voiceIndex)129 private String getVoiceName(int voiceIndex) { 130 assert mInitialized; 131 return mVoices.get(voiceIndex).mName; 132 } 133 134 /** 135 * @return the language of the voice at a given index. 136 */ 137 @CalledByNative getVoiceLanguage(int voiceIndex)138 private String getVoiceLanguage(int voiceIndex) { 139 assert mInitialized; 140 return mVoices.get(voiceIndex).mLanguage; 141 } 142 143 /** 144 * Attempt to start speaking an utterance. If it returns true, will call back on 145 * start and end. 146 * 147 * @param utteranceId A unique id for this utterance so that callbacks can be tied 148 * to a particular utterance. 149 * @param text The text to speak. 150 * @param lang The language code for the text (e.g., "en-US"). 151 * @param rate The speech rate, in the units expected by Android TextToSpeech. 152 * @param pitch The speech pitch, in the units expected by Android TextToSpeech. 153 * @param volume The speech volume, in the units expected by Android TextToSpeech. 154 * @return true on success. 155 */ 156 @CalledByNative speak( int utteranceId, String text, String lang, float rate, float pitch, float volume)157 private boolean speak( 158 int utteranceId, String text, String lang, float rate, float pitch, float volume) { 159 if (!mInitialized) { 160 mPendingUtterance = 161 new PendingUtterance(this, utteranceId, text, lang, rate, pitch, volume); 162 return true; 163 } 164 if (mPendingUtterance != null) mPendingUtterance = null; 165 166 if (!lang.equals(mCurrentLanguage)) { 167 mTextToSpeech.setLanguage(new Locale(lang)); 168 mCurrentLanguage = lang; 169 } 170 171 mTextToSpeech.setSpeechRate(rate); 172 mTextToSpeech.setPitch(pitch); 173 174 int result = callSpeak(text, volume, utteranceId); 175 return (result == TextToSpeech.SUCCESS); 176 } 177 178 /** 179 * Stop the current utterance. 180 */ 181 @CalledByNative stop()182 private void stop() { 183 if (mInitialized) mTextToSpeech.stop(); 184 if (mPendingUtterance != null) mPendingUtterance = null; 185 } 186 187 /** 188 * Post a task to the UI thread to send the TTS "end" event. 189 */ sendEndEventOnUiThread(final String utteranceId)190 private void sendEndEventOnUiThread(final String utteranceId) { 191 PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> { 192 if (mNativeTtsPlatformImplAndroid != 0) { 193 TtsPlatformImplJni.get().onEndEvent( 194 mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); 195 } 196 }); 197 } 198 199 /** 200 * Post a task to the UI thread to send the TTS "error" event. 201 */ sendErrorEventOnUiThread(final String utteranceId)202 private void sendErrorEventOnUiThread(final String utteranceId) { 203 PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> { 204 if (mNativeTtsPlatformImplAndroid != 0) { 205 TtsPlatformImplJni.get().onErrorEvent( 206 mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); 207 } 208 }); 209 } 210 211 /** 212 * Post a task to the UI thread to send the TTS "start" event. 213 */ sendStartEventOnUiThread(final String utteranceId)214 private void sendStartEventOnUiThread(final String utteranceId) { 215 PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, () -> { 216 if (mNativeTtsPlatformImplAndroid != 0) { 217 TtsPlatformImplJni.get().onStartEvent( 218 mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId)); 219 } 220 }); 221 } 222 223 @SuppressWarnings("deprecation") addOnUtteranceProgressListener()224 private void addOnUtteranceProgressListener() { 225 mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { 226 @Override 227 public void onDone(final String utteranceId) { 228 sendEndEventOnUiThread(utteranceId); 229 } 230 231 @Override 232 public void onError(final String utteranceId, int errorCode) { 233 sendErrorEventOnUiThread(utteranceId); 234 } 235 236 @Override 237 @Deprecated 238 public void onError(final String utteranceId) {} 239 240 @Override 241 public void onStart(final String utteranceId) { 242 sendStartEventOnUiThread(utteranceId); 243 } 244 }); 245 } 246 247 @SuppressWarnings("deprecation") callSpeak(String text, float volume, int utteranceId)248 private int callSpeak(String text, float volume, int utteranceId) { 249 Bundle params = new Bundle(); 250 if (volume != 1.0) { 251 params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume); 252 } 253 return mTextToSpeech.speak( 254 text, TextToSpeech.QUEUE_FLUSH, params, Integer.toString(utteranceId)); 255 } 256 257 /** 258 * Note: we enforce that this method is called on the UI thread, so 259 * we can call TtsPlatformImplJni.get().voicesChanged directly. 260 */ initialize()261 private void initialize() { 262 TraceEvent.startAsync("TtsPlatformImpl:initialize", hashCode()); 263 264 new AsyncTask<List<TtsVoice>>() { 265 @Override 266 protected List<TtsVoice> doInBackground() { 267 assert mNativeTtsPlatformImplAndroid != 0; 268 269 try (TraceEvent te = TraceEvent.scoped("TtsPlatformImpl:initialize.async_task")) { 270 Locale[] locales = Locale.getAvailableLocales(); 271 final List<TtsVoice> voices = new ArrayList<>(); 272 for (Locale locale : locales) { 273 if (!locale.getVariant().isEmpty()) continue; 274 try { 275 if (mTextToSpeech.isLanguageAvailable(locale) > 0) { 276 String name = locale.getDisplayLanguage(); 277 if (!locale.getCountry().isEmpty()) { 278 name += " " + locale.getDisplayCountry(); 279 } 280 TtsVoice voice = new TtsVoice(name, locale.toString()); 281 voices.add(voice); 282 } 283 } catch (Exception e) { 284 // Just skip the locale if it's invalid. 285 // 286 // We used to catch only java.util.MissingResourceException, 287 // but we need to catch more exceptions to work around a bug 288 // in Google TTS when we query "bn". 289 // http://crbug.com/792856 290 } 291 } 292 return voices; 293 } 294 } 295 296 @Override 297 protected void onPostExecute(List<TtsVoice> voices) { 298 mVoices = voices; 299 mInitialized = true; 300 301 TtsPlatformImplJni.get().voicesChanged(mNativeTtsPlatformImplAndroid); 302 303 if (mPendingUtterance != null) mPendingUtterance.speak(); 304 305 TraceEvent.finishAsync( 306 "TtsPlatformImpl:initialize", TtsPlatformImpl.this.hashCode()); 307 } 308 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 309 } 310 311 @NativeMethods 312 interface Natives { voicesChanged(long nativeTtsPlatformImplAndroid)313 void voicesChanged(long nativeTtsPlatformImplAndroid); onEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId)314 void onEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId); onStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId)315 void onStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId); onErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId)316 void onErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId); 317 } 318 } 319