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