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