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