1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil -*- */
2 /* vim: set ts=20 sts=4 et sw=4: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 package org.mozilla.gecko;
8 
9 import org.mozilla.gecko.annotation.WrapForJNI;
10 import org.mozilla.gecko.util.ThreadUtils;
11 
12 import android.content.Context;
13 import android.os.Build;
14 import android.speech.tts.TextToSpeech;
15 import android.speech.tts.UtteranceProgressListener;
16 import android.util.Log;
17 
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.Locale;
21 import java.util.Set;
22 import java.util.UUID;
23 import java.util.concurrent.atomic.AtomicBoolean;
24 
25 public class SpeechSynthesisService  {
26     private static final String LOGTAG = "GeckoSpeechSynthesis";
27     // Object type is used to make it easier to remove android.speech dependencies using Proguard.
28     private static Object sTTS;
29 
30     @WrapForJNI(calledFrom = "gecko")
initSynth()31     public static void initSynth() {
32         initSynthInternal();
33     }
34 
35     // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
initSynthInternal()36     private static void initSynthInternal() {
37         if (sTTS != null) {
38             return;
39         }
40 
41         final Context ctx = GeckoAppShell.getApplicationContext();
42 
43         sTTS = new TextToSpeech(ctx, new TextToSpeech.OnInitListener() {
44             @Override
45             public void onInit(final int status) {
46                 if (status != TextToSpeech.SUCCESS) {
47                     Log.w(LOGTAG, "Failed to initialize TextToSpeech");
48                     return;
49                 }
50 
51                 setUtteranceListener();
52                 registerVoicesByLocale();
53             }
54         });
55     }
56 
getTTS()57     private static TextToSpeech getTTS() {
58         return (TextToSpeech) sTTS;
59     }
60 
registerVoicesByLocale()61     private static void registerVoicesByLocale() {
62         ThreadUtils.postToBackgroundThread(new Runnable() {
63             @Override
64             public void run() {
65                 final TextToSpeech tss = getTTS();
66                 final Locale defaultLocale = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
67                         ? tss.getDefaultLanguage()
68                         : tss.getLanguage();
69                 for (final Locale locale : getAvailableLanguages()) {
70                     final Set<String> features = tss.getFeatures(locale);
71                     final boolean isLocal = features != null && features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
72                     final String localeStr = locale.toString();
73                     registerVoice("moz-tts:android:" + localeStr, locale.getDisplayName(), localeStr.replace("_", "-"), !isLocal, defaultLocale == locale);
74                 }
75                 doneRegisteringVoices();
76             }
77         });
78     }
79 
getAvailableLanguages()80     private static Set<Locale> getAvailableLanguages() {
81         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
82             // While this method was introduced in 21, it seems that it
83             // has not been implemented in the speech service side until 23.
84             final Set<Locale> availableLanguages = getTTS().getAvailableLanguages();
85             if (availableLanguages != null) {
86                 return availableLanguages;
87             }
88         }
89         final Set<Locale> locales = new HashSet<Locale>();
90         for (final Locale locale : Locale.getAvailableLocales()) {
91             if (locale.getVariant().isEmpty() && getTTS().isLanguageAvailable(locale) > 0) {
92                 locales.add(locale);
93             }
94         }
95 
96         return locales;
97     }
98 
99     @WrapForJNI(dispatchTo = "gecko")
registerVoice(String uri, String name, String locale, boolean isNetwork, boolean isDefault)100     private static native void registerVoice(String uri, String name, String locale, boolean isNetwork, boolean isDefault);
101 
102     @WrapForJNI(dispatchTo = "gecko")
doneRegisteringVoices()103     private static native void doneRegisteringVoices();
104 
105     @WrapForJNI(calledFrom = "gecko")
speak(final String uri, final String text, final float rate, final float pitch, final float volume)106     public static String speak(final String uri, final String text, final float rate,
107                                final float pitch, final float volume) {
108         final AtomicBoolean result = new AtomicBoolean(false);
109         final String utteranceId = UUID.randomUUID().toString();
110         speakInternal(uri, text, rate, pitch, volume, utteranceId, result);
111         return result.get() ? utteranceId : null;
112     }
113 
114     // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
speakInternal(final String uri, final String text, final float rate, final float pitch, final float volume, final String utteranceId, final AtomicBoolean result)115     private static void speakInternal(final String uri, final String text, final float rate,
116                                       final float pitch, final float volume, final String utteranceId, final AtomicBoolean result) {
117         if (sTTS == null) {
118             Log.w(LOGTAG, "TextToSpeech is not initialized");
119             return;
120         }
121 
122         final HashMap<String, String> params = new HashMap<String, String>();
123         params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(volume));
124         params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
125         final TextToSpeech tss = (TextToSpeech) sTTS;
126         tss.setLanguage(new Locale(uri.substring("moz-tts:android:".length())));
127         tss.setSpeechRate(rate);
128         tss.setPitch(pitch);
129         final int speakRes = tss.speak(text, TextToSpeech.QUEUE_FLUSH, params);
130         result.set(speakRes == TextToSpeech.SUCCESS);
131     }
132 
setUtteranceListener()133     private static void setUtteranceListener() {
134         if (sTTS == null) {
135             Log.w(LOGTAG, "TextToSpeech is not initialized");
136             return;
137         }
138 
139         getTTS().setOnUtteranceProgressListener(new UtteranceProgressListener() {
140             @Override
141             public void onDone(final String utteranceId) {
142                 dispatchEnd(utteranceId);
143             }
144 
145             @Override
146             public void onError(final String utteranceId) {
147                 dispatchError(utteranceId);
148             }
149 
150             @Override
151             public void onStart(final String utteranceId) {
152                 dispatchStart(utteranceId);
153             }
154 
155             @Override
156             public void onStop(final String utteranceId, final boolean interrupted) {
157                 if (interrupted) {
158                     dispatchEnd(utteranceId);
159                 } else {
160                     // utterance isn't started yet.
161                     dispatchError(utteranceId);
162                 }
163             }
164 
165             public void onRangeStart(final String utteranceId, final int start, final int end,
166                                      final int frame) {
167                 dispatchBoundary(utteranceId, start, end);
168             }
169         });
170     }
171 
172     @WrapForJNI(dispatchTo = "gecko")
dispatchStart(String utteranceId)173     private static native void dispatchStart(String utteranceId);
174 
175     @WrapForJNI(dispatchTo = "gecko")
dispatchEnd(String utteranceId)176     private static native void dispatchEnd(String utteranceId);
177 
178     @WrapForJNI(dispatchTo = "gecko")
dispatchError(String utteranceId)179     private static native void dispatchError(String utteranceId);
180 
181     @WrapForJNI(dispatchTo = "gecko")
dispatchBoundary(String utteranceId, int start, int end)182     private static native void dispatchBoundary(String utteranceId, int start, int end);
183 
184     @WrapForJNI(calledFrom = "gecko")
stop()185     public static void stop() {
186         stopInternal();
187     }
188 
189     // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
stopInternal()190     private static void stopInternal() {
191         if (sTTS == null) {
192             Log.w(LOGTAG, "TextToSpeech is not initialized");
193             return;
194         }
195 
196         getTTS().stop();
197         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
198             // Android M has onStop method.  If Android L or above, dispatch
199             // event
200             dispatchEnd(null);
201         }
202     }
203 }
204