1 | // Copyright (c) 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.chrome.browser; |
6 | |
7 | import android.content.Context; |
8 | import android.speech.tts.TextToSpeech; |
9 | import android.speech.tts.UtteranceProgressListener; |
10 | import java.lang.Double; |
11 | import java.lang.Integer; |
12 | import java.util.ArrayList; |
13 | import java.util.HashMap; |
14 | import java.util.Locale; |
15 | import org.chromium.base.CalledByNative; |
16 | import org.chromium.base.ThreadUtils; |
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 | */ |
27 | class 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 int mNativeTtsPlatformImplAndroid; |
38 | private final TextToSpeech mTextToSpeech; |
39 | private boolean mInitialized; |
40 | private ArrayList<TtsVoice> mVoices; |
41 | private String mCurrentLanguage; |
42 | |
43 | private TtsPlatformImpl(int nativeTtsPlatformImplAndroid, Context context) { |
44 | mInitialized = false; |
45 | mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid; |
46 | mTextToSpeech = new TextToSpeech(context, new TextToSpeech.OnInitListener() { |
47 | public void onInit(int status) { |
48 | if (status == TextToSpeech.SUCCESS) { |
49 | ThreadUtils.runOnUiThread(new Runnable() { |
50 | @Override |
51 | public void run() { |
52 | initialize(); |
53 | } |
54 | }); |
55 | } |
56 | } |
57 | }); |
58 | mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { |
59 | public void onDone(final String utteranceId) { |
60 | ThreadUtils.runOnUiThread(new Runnable() { |
61 | @Override |
62 | public void run() { |
63 | if (mNativeTtsPlatformImplAndroid != 0) { |
64 | nativeOnEndEvent(mNativeTtsPlatformImplAndroid, |
65 | Integer.parseInt(utteranceId)); |
66 | } |
67 | } |
68 | }); |
69 | } |
70 | |
71 | public void onError(final String utteranceId) { |
72 | ThreadUtils.runOnUiThread(new Runnable() { |
73 | @Override |
74 | public void run() { |
75 | if (mNativeTtsPlatformImplAndroid != 0) { |
76 | nativeOnErrorEvent(mNativeTtsPlatformImplAndroid, |
77 | Integer.parseInt(utteranceId)); |
78 | } |
79 | } |
80 | }); |
81 | } |
82 | |
83 | public void onStart(final String utteranceId) { |
84 | ThreadUtils.runOnUiThread(new Runnable() { |
85 | @Override |
86 | public void run() { |
87 | if (mNativeTtsPlatformImplAndroid != 0) { |
88 | nativeOnStartEvent(mNativeTtsPlatformImplAndroid, |
89 | Integer.parseInt(utteranceId)); |
90 | } |
91 | } |
92 | }); |
93 | } |
94 | }); |
95 | }; |
96 | |
97 | /** |
98 | * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid |
99 | * on the C++ side. |
100 | * |
101 | * @param nativeTtsPlatformImplAndroid The C++ object that owns us. |
102 | * @param context The app context. |
103 | */ |
104 | @CalledByNative |
105 | private static TtsPlatformImpl create(int nativeTtsPlatformImplAndroid, |
106 | Context context) { |
107 | return new TtsPlatformImpl(nativeTtsPlatformImplAndroid, context); |
108 | } |
109 | |
110 | /** |
111 | * Called when our C++ counterpoint is deleted. Clear the handle to our |
112 | * native C++ object, ensuring it's never called. |
113 | */ |
114 | @CalledByNative |
115 | private void destroy() { |
116 | mNativeTtsPlatformImplAndroid = 0; |
117 | } |
118 | |
119 | /** |
120 | * @return true if our TextToSpeech object is initialized and we've |
121 | * finished scanning the list of voices. |
122 | */ |
123 | @CalledByNative |
124 | private boolean isInitialized() { |
125 | return mInitialized; |
126 | } |
127 | |
128 | /** |
129 | * @return the number of voices. |
130 | */ |
131 | @CalledByNative |
132 | private int getVoiceCount() { |
133 | assert mInitialized == true; |
134 | return mVoices.size(); |
135 | } |
136 | |
137 | /** |
138 | * @return the name of the voice at a given index. |
139 | */ |
140 | @CalledByNative |
141 | private String getVoiceName(int voiceIndex) { |
142 | assert mInitialized == true; |
143 | return mVoices.get(voiceIndex).mName; |
144 | } |
145 | |
146 | /** |
147 | * @return the language of the voice at a given index. |
148 | */ |
149 | @CalledByNative |
150 | private String getVoiceLanguage(int voiceIndex) { |
151 | assert mInitialized == true; |
152 | return mVoices.get(voiceIndex).mLanguage; |
153 | } |
154 | |
155 | /** |
156 | * Attempt to start speaking an utterance. If it returns true, will call back on |
157 | * start and end. |
158 | * |
159 | * @param utteranceId A unique id for this utterance so that callbacks can be tied |
160 | * to a particular utterance. |
161 | * @param text The text to speak. |
162 | * @param lang The language code for the text (e.g., "en-US"). |
163 | * @param rate The speech rate, in the units expected by Android TextToSpeech. |
164 | * @param pitch The speech pitch, in the units expected by Android TextToSpeech. |
165 | * @param volume The speech volume, in the units expected by Android TextToSpeech. |
166 | * @return true on success. |
167 | */ |
168 | @CalledByNative |
169 | private boolean speak(int utteranceId, String text, String lang, |
170 | float rate, float pitch, float volume) { |
171 | assert mInitialized == true; |
172 | if (!lang.equals(mCurrentLanguage)) { |
173 | mTextToSpeech.setLanguage(new Locale(lang)); |
174 | mCurrentLanguage = lang; |
175 | } |
176 | |
177 | mTextToSpeech.setSpeechRate(rate); |
178 | mTextToSpeech.setPitch(pitch); |
179 | HashMap<String, String> params = new HashMap<String, String>(); |
180 | if (volume != 1.0) { |
181 | params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume)); |
182 | } |
183 | params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(utteranceId)); |
184 | int result = mTextToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params); |
185 | return (result == TextToSpeech.SUCCESS); |
186 | } |
187 | |
188 | /** |
189 | * Stop the current utterance. |
190 | */ |
191 | @CalledByNative |
192 | private void stop() { |
193 | assert mInitialized == true; |
194 | mTextToSpeech.stop(); |
195 | } |
196 | |
197 | /** |
198 | * Note: we enforce that this method is called on the UI thread, so |
199 | * we can call nativeVoicesChanged directly. |
200 | */ |
201 | private void initialize() { |
202 | assert mNativeTtsPlatformImplAndroid != 0; |
203 | |
204 | // Note: Android supports multiple speech engines, but querying the |
205 | // metadata about all of them is expensive. So we deliberately only |
206 | // support the default speech engine, and expose the different |
207 | // supported languages for the default engine as different voices. |
208 | String defaultEngineName = mTextToSpeech.getDefaultEngine(); |
209 | String engineLabel = defaultEngineName; |
210 | for (TextToSpeech.EngineInfo info : mTextToSpeech.getEngines()) { |
211 | if (info.name.equals(defaultEngineName)) engineLabel = info.label; |
212 | } |
213 | Locale[] locales = Locale.getAvailableLocales(); |
214 | mVoices = new ArrayList<TtsVoice>(); |
215 | for (int i = 0; i < locales.length; ++i) { |
216 | if (!locales[i].getVariant().isEmpty()) continue; |
217 | if (mTextToSpeech.isLanguageAvailable(locales[i]) > 0) { |
218 | String name = locales[i].getDisplayLanguage(); |
219 | if (!locales[i].getCountry().isEmpty()) { |
220 | name += " " + locales[i].getDisplayCountry(); |
221 | } |
222 | TtsVoice voice = new TtsVoice(name, locales[i].toString()); |
223 | mVoices.add(voice); |
224 | } |
225 | } |
226 | |
227 | mInitialized = true; |
228 | nativeVoicesChanged(mNativeTtsPlatformImplAndroid); |
229 | } |
230 | |
231 | private native void nativeVoicesChanged(int nativeTtsPlatformImplAndroid); |
232 | private native void nativeOnEndEvent(int nativeTtsPlatformImplAndroid, int utteranceId); |
233 | private native void nativeOnStartEvent(int nativeTtsPlatformImplAndroid, int utteranceId); |
234 | private native void nativeOnErrorEvent(int nativeTtsPlatformImplAndroid, int utteranceId); |
235 | } |