summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java227
1 files changed, 227 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
new file mode 100644
index 0000000000..9bb116451e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
@@ -0,0 +1,227 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil -*- */
+/* vim: set ts=20 sts=4 et sw=4: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.Context;
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.util.Log;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class SpeechSynthesisService {
+ private static final String LOGTAG = "GeckoSpeechSynthesis";
+ // Object type is used to make it easier to remove android.speech dependencies using Proguard.
+ private static Object sTTS;
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void initSynth() {
+ initSynthInternal();
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void initSynthInternal() {
+ if (sTTS != null) {
+ return;
+ }
+
+ final Context ctx = GeckoAppShell.getApplicationContext();
+
+ sTTS =
+ new TextToSpeech(
+ ctx,
+ new TextToSpeech.OnInitListener() {
+ @Override
+ public void onInit(final int status) {
+ if (status != TextToSpeech.SUCCESS) {
+ Log.w(LOGTAG, "Failed to initialize TextToSpeech");
+ return;
+ }
+
+ setUtteranceListener();
+ registerVoicesByLocale();
+ }
+ });
+ }
+
+ private static TextToSpeech getTTS() {
+ return (TextToSpeech) sTTS;
+ }
+
+ private static void registerVoicesByLocale() {
+ ThreadUtils.postToBackgroundThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ final TextToSpeech tss = getTTS();
+ if (tss == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+ final Locale defaultLocale = tss.getDefaultLanguage();
+ for (final Locale locale : getAvailableLanguages()) {
+ final Set<String> features = tss.getFeatures(locale);
+ final boolean isLocal =
+ features != null
+ && features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
+ final String localeStr = locale.toString();
+ registerVoice(
+ "moz-tts:android:" + localeStr,
+ locale.getDisplayName(),
+ localeStr.replace("_", "-"),
+ !isLocal,
+ defaultLocale == locale);
+ }
+ doneRegisteringVoices();
+ }
+ });
+ }
+
+ private static Set<Locale> getAvailableLanguages() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // While this method was introduced in 21, it seems that it
+ // has not been implemented in the speech service side until 23.
+ final Set<Locale> availableLanguages = getTTS().getAvailableLanguages();
+ if (availableLanguages != null) {
+ return availableLanguages;
+ }
+ }
+ final Set<Locale> locales = new HashSet<Locale>();
+ for (final Locale locale : Locale.getAvailableLocales()) {
+ if (locale.getVariant().isEmpty() && getTTS().isLanguageAvailable(locale) > 0) {
+ locales.add(locale);
+ }
+ }
+
+ return locales;
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void registerVoice(
+ String uri, String name, String locale, boolean isNetwork, boolean isDefault);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void doneRegisteringVoices();
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String speak(
+ final String uri,
+ final String text,
+ final float rate,
+ final float pitch,
+ final float volume) {
+ final AtomicBoolean result = new AtomicBoolean(false);
+ final String utteranceId = UUID.randomUUID().toString();
+ speakInternal(uri, text, rate, pitch, volume, utteranceId, result);
+ return result.get() ? utteranceId : null;
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void speakInternal(
+ final String uri,
+ final String text,
+ final float rate,
+ final float pitch,
+ final float volume,
+ final String utteranceId,
+ final AtomicBoolean result) {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ final HashMap<String, String> params = new HashMap<String, String>();
+ params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(volume));
+ params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
+ final TextToSpeech tss = (TextToSpeech) sTTS;
+ tss.setLanguage(new Locale(uri.substring("moz-tts:android:".length())));
+ tss.setSpeechRate(rate);
+ tss.setPitch(pitch);
+ final int speakRes = tss.speak(text, TextToSpeech.QUEUE_FLUSH, params);
+ result.set(speakRes == TextToSpeech.SUCCESS);
+ }
+
+ private static void setUtteranceListener() {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ getTTS()
+ .setOnUtteranceProgressListener(
+ new UtteranceProgressListener() {
+ @Override
+ public void onDone(final String utteranceId) {
+ dispatchEnd(utteranceId);
+ }
+
+ @Override
+ public void onError(final String utteranceId) {
+ dispatchError(utteranceId);
+ }
+
+ @Override
+ public void onStart(final String utteranceId) {
+ dispatchStart(utteranceId);
+ }
+
+ @Override
+ public void onStop(final String utteranceId, final boolean interrupted) {
+ if (interrupted) {
+ dispatchEnd(utteranceId);
+ } else {
+ // utterance isn't started yet.
+ dispatchError(utteranceId);
+ }
+ }
+
+ public void onRangeStart(
+ final String utteranceId, final int start, final int end, final int frame) {
+ dispatchBoundary(utteranceId, start, end);
+ }
+ });
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchStart(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchEnd(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchError(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchBoundary(String utteranceId, int start, int end);
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void stop() {
+ stopInternal();
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void stopInternal() {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ getTTS().stop();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Android M has onStop method. If Android L or above, dispatch
+ // event
+ dispatchEnd(null);
+ }
+ }
+}