Bug 1184142 - Support WebSpeech in GeckoView. r=snorp r=smaug
authorEitan Isaacson <eitan@monotonous.org>
Fri, 16 Mar 2018 12:08:00 +0200
changeset 408581 00ca51f4fafc584260b05fbe7577ba675d73f6b5
parent 408580 30458407e1e4b9914a9dbddcade2da9a18f9bbd2
child 408582 a66043ecf949b365d7a3c09423fb0f4ea137668f
push id100983
push userdluca@mozilla.com
push dateFri, 16 Mar 2018 21:46:04 +0000
treeherdermozilla-inbound@231448afb53d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, smaug
bugs1184142
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1184142 - Support WebSpeech in GeckoView. r=snorp r=smaug
dom/media/webspeech/synth/android/AndroidSpeechModule.cpp
dom/media/webspeech/synth/android/SpeechSynthesisService.cpp
dom/media/webspeech/synth/android/SpeechSynthesisService.h
dom/media/webspeech/synth/android/moz.build
dom/media/webspeech/synth/moz.build
mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
widget/android/GeneratedJNINatives.h
widget/android/GeneratedJNIWrappers.cpp
widget/android/GeneratedJNIWrappers.h
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/android/AndroidSpeechModule.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/ModuleUtils.h"
+#include "SpeechSynthesisService.h"
+
+using namespace mozilla::dom;
+
+#define ANDROIDSPEECHSERVICE_CID \
+  {0x311b2dab, 0xf4d3, 0x4be4, {0x81, 0x23, 0x67, 0x32, 0x31, 0x3d, 0x95, 0xc2}}
+
+#define ANDROIDSPEECHSERVICE_CONTRACTID "@mozilla.org/androidspeechsynth;1"
+
+// Defines AndroidSpeechServiceConstructor
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(SpeechSynthesisService,
+                                         SpeechSynthesisService::GetInstanceForService)
+
+// Defines kANDROIDSPEECHSERVICE_CID
+NS_DEFINE_NAMED_CID(ANDROIDSPEECHSERVICE_CID);
+
+static const mozilla::Module::CIDEntry kCIDs[] = {
+  { &kANDROIDSPEECHSERVICE_CID, true, nullptr, SpeechSynthesisServiceConstructor },
+  { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kContracts[] = {
+  { ANDROIDSPEECHSERVICE_CONTRACTID, &kANDROIDSPEECHSERVICE_CID },
+  { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kCategories[] = {
+  { "speech-synth-started", "Android Speech Synth", ANDROIDSPEECHSERVICE_CONTRACTID },
+  { nullptr }
+};
+
+static const mozilla::Module kModule = {
+  mozilla::Module::kVersion,
+  kCIDs,
+  kContracts,
+  kCategories,
+  nullptr,
+  nullptr,
+  nullptr,
+};
+
+NSMODULE_DEFN(androidspeechsynth) = &kModule;
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/android/SpeechSynthesisService.cpp
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "SpeechSynthesisService.h"
+
+#include <android/log.h>
+
+#include "nsXULAppAPI.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/jni/Utils.h"
+#include "mozilla/Preferences.h"
+
+#define ALOG(args...)                                                          \
+  __android_log_print(ANDROID_LOG_INFO, "GeckoSpeechSynthesis", ##args)
+
+namespace mozilla {
+namespace dom {
+
+StaticRefPtr<SpeechSynthesisService> SpeechSynthesisService::sSingleton;
+
+class AndroidSpeechCallback final : public nsISpeechTaskCallback
+{
+public:
+  AndroidSpeechCallback()
+  {
+  }
+
+  NS_DECL_ISUPPORTS
+
+  NS_IMETHOD OnResume() override { return NS_OK; }
+
+  NS_IMETHOD OnPause() override { return NS_OK; }
+
+  NS_IMETHOD OnCancel() override
+  {
+    java::SpeechSynthesisService::Stop();
+    return NS_OK;
+  }
+
+  NS_IMETHOD OnVolumeChanged(float aVolume) override { return NS_OK; }
+
+private:
+  ~AndroidSpeechCallback() {}
+};
+
+NS_IMPL_ISUPPORTS(AndroidSpeechCallback, nsISpeechTaskCallback)
+
+NS_IMPL_ISUPPORTS(SpeechSynthesisService, nsISpeechService)
+
+void
+SpeechSynthesisService::Setup()
+{
+  ALOG("SpeechSynthesisService::Setup");
+
+  if (!Preferences::GetBool("media.webspeech.synth.enabled") ||
+      Preferences::GetBool("media.webspeech.synth.test")) {
+    return;
+  }
+
+  if (!jni::IsAvailable()) {
+    NS_WARNING("Failed to initialize speech synthesis");
+    return;
+  }
+
+  Init();
+  java::SpeechSynthesisService::InitSynth();
+}
+
+// nsISpeechService
+
+NS_IMETHODIMP
+SpeechSynthesisService::Speak(const nsAString& aText,
+                              const nsAString& aUri,
+                              float aVolume,
+                              float aRate,
+                              float aPitch,
+                              nsISpeechTask* aTask)
+{
+  if (mTask) {
+    NS_WARNING("Service only supports one speech task at a time.");
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  RefPtr<AndroidSpeechCallback> callback = new AndroidSpeechCallback();
+  nsresult rv = aTask->Setup(callback);
+
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  jni::String::LocalRef utteranceId =
+    java::SpeechSynthesisService::Speak(aUri, aText, aRate, aPitch, aVolume);
+  if (!utteranceId) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  mTaskUtteranceId = utteranceId->ToCString();
+  mTask = aTask;
+  mTaskTextLength = aText.Length();
+  mTaskTextOffset = 0;
+
+  return NS_OK;
+}
+
+SpeechSynthesisService*
+SpeechSynthesisService::GetInstance(bool aCreate)
+{
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    MOZ_ASSERT(
+      false,
+      "SpeechSynthesisService can only be started on main gecko process");
+    return nullptr;
+  }
+
+  if (!sSingleton && aCreate) {
+    sSingleton = new SpeechSynthesisService();
+    sSingleton->Setup();
+    ClearOnShutdown(&sSingleton);
+  }
+
+  return sSingleton;
+}
+
+already_AddRefed<SpeechSynthesisService>
+SpeechSynthesisService::GetInstanceForService()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  RefPtr<SpeechSynthesisService> sapiService = GetInstance();
+  return sapiService.forget();
+}
+
+// JNI
+
+void
+SpeechSynthesisService::RegisterVoice(jni::String::Param aUri,
+                                      jni::String::Param aName,
+                                      jni::String::Param aLocale,
+                                      bool aIsNetwork,
+                                      bool aIsDefault)
+{
+  nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance();
+  SpeechSynthesisService* service = SpeechSynthesisService::GetInstance(false);
+  // This service can only speak one utterance at a time, so we set
+  // aQueuesUtterances to true in order to track global state and schedule
+  // access to this service.
+  DebugOnly<nsresult> rv = registry->AddVoice(service,
+                                              aUri->ToString(),
+                                              aName->ToString(),
+                                              aLocale->ToString(),
+                                              !aIsNetwork,
+                                              true);
+
+  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice");
+
+  if (aIsDefault) {
+    DebugOnly<nsresult> rv = registry->SetDefaultVoice(aUri->ToString(), true);
+
+    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to set voice as default");
+  }
+}
+
+void
+SpeechSynthesisService::DoneRegisteringVoices()
+{
+  nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance();
+  registry->NotifyVoicesChanged();
+}
+
+void
+SpeechSynthesisService::DispatchStart(jni::String::Param aUtteranceId)
+{
+  if (sSingleton) {
+    MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+    nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+    if (task) {
+      sSingleton->mTaskStartTime = TimeStamp::Now();
+      DebugOnly<nsresult> rv = task->DispatchStart();
+      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+    }
+  }
+}
+
+void
+SpeechSynthesisService::DispatchEnd(jni::String::Param aUtteranceId)
+{
+  if (sSingleton) {
+    // In API older than 23, we will sometimes call this function
+    // without providing an utterance ID.
+    MOZ_ASSERT(!aUtteranceId ||
+               sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+    nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+    sSingleton->mTask = nullptr;
+    if (task) {
+      TimeStamp startTime = sSingleton->mTaskStartTime;
+      DebugOnly<nsresult> rv = task->DispatchEnd(
+        (TimeStamp::Now() - startTime).ToSeconds(), sSingleton->mTaskTextLength);
+      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+    }
+  }
+}
+
+void
+SpeechSynthesisService::DispatchError(jni::String::Param aUtteranceId)
+{
+  if (sSingleton) {
+    MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+    nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+    sSingleton->mTask = nullptr;
+    if (task) {
+      TimeStamp startTime = sSingleton->mTaskStartTime;
+      DebugOnly<nsresult> rv = task->DispatchError(
+        (TimeStamp::Now() - startTime).ToSeconds(), sSingleton->mTaskTextOffset);
+      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+    }
+  }
+}
+
+void
+SpeechSynthesisService::DispatchBoundary(jni::String::Param aUtteranceId,
+                                         int32_t aStart,
+                                         int32_t aEnd)
+{
+  if (sSingleton) {
+    MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+    nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+    if (task) {
+      TimeStamp startTime = sSingleton->mTaskStartTime;
+      sSingleton->mTaskTextOffset = aStart;
+      DebugOnly<nsresult> rv =
+        task->DispatchBoundary(NS_LITERAL_STRING("word"),
+                               (TimeStamp::Now() - startTime).ToSeconds(),
+                               aStart,
+                               aEnd - aStart,
+                               1);
+      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch boundary");
+    }
+  }
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/android/SpeechSynthesisService.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_SpeechSynthesisService_h
+#define mozilla_dom_SpeechSynthesisService_h
+
+#include "GeneratedJNINatives.h"
+#include "nsISpeechService.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class SpeechSynthesisService final
+  : public nsISpeechService
+  , public java::SpeechSynthesisService::Natives<SpeechSynthesisService>
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISPEECHSERVICE
+
+  SpeechSynthesisService(){};
+
+  void Setup();
+
+  static void DoneRegisteringVoices();
+
+  static void RegisterVoice(jni::String::Param aUri,
+                            jni::String::Param aName,
+                            jni::String::Param aLocale,
+                            bool aIsNetwork,
+                            bool aIsDefault);
+
+  static void DispatchStart(jni::String::Param aUtteranceId);
+
+  static void DispatchEnd(jni::String::Param aUtteranceId);
+
+  static void DispatchError(jni::String::Param aUtteranceId);
+
+  static void DispatchBoundary(jni::String::Param aUtteranceId,
+                               int32_t aStart,
+                               int32_t aEnd);
+
+  static SpeechSynthesisService* GetInstance(bool aCreate = true);
+  static already_AddRefed<SpeechSynthesisService> GetInstanceForService();
+
+  static StaticRefPtr<SpeechSynthesisService> sSingleton;
+
+private:
+  virtual ~SpeechSynthesisService(){};
+
+  nsCOMPtr<nsISpeechTask> mTask;
+
+  // Unique ID assigned to utterance when it is sent to system service.
+  nsCString mTaskUtteranceId;
+
+  // Time stamp from the moment the utterance is started.
+  TimeStamp mTaskStartTime;
+
+  // Length of text of the utterance.
+  uint32_t mTaskTextLength;
+
+  // Current offset in characters of what has been spoken.
+  uint32_t mTaskTextOffset;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/android/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla.dom += ['SpeechSynthesisService.h']
+
+UNIFIED_SOURCES += [
+  'AndroidSpeechModule.cpp',
+  'SpeechSynthesisService.cpp'
+]
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
--- a/dom/media/webspeech/synth/moz.build
+++ b/dom/media/webspeech/synth/moz.build
@@ -43,16 +43,19 @@ if CONFIG['MOZ_WEBSPEECH']:
         ]
 
     if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
         DIRS += ['windows']
 
     if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
         DIRS += ['cocoa']
 
+    if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
+        DIRS += ['android']
+
     if CONFIG['MOZ_SYNTH_SPEECHD']:
         DIRS += ['speechd']
 
 IPDL_SOURCES += [
     'ipc/PSpeechSynthesis.ipdl',
     'ipc/PSpeechSynthesisRequest.ipdl',
 ]
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
@@ -0,0 +1,159 @@
+/* -*- 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 org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.util.Log;
+
+import java.lang.System;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+
+public class SpeechSynthesisService  {
+    private static final String LOGTAG = "GeckoSpeechSynthesis";
+    private static TextToSpeech sTTS;
+
+    @WrapForJNI(calledFrom = "gecko")
+    public static void initSynth() {
+        if (sTTS != null) {
+            return;
+        }
+
+        final Context ctx = GeckoAppShell.getApplicationContext();
+
+        sTTS = new TextToSpeech(ctx, new TextToSpeech.OnInitListener() {
+            @Override
+            public void onInit(int status) {
+                if (status != TextToSpeech.SUCCESS) {
+                    Log.w(LOGTAG, "Failed to initialize TextToSpeech");
+                    return;
+                }
+
+                setUtteranceListener();
+                registerVoicesByLocale();
+            }
+        });
+    }
+
+    private static void registerVoicesByLocale() {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Locale defaultLocale = sTTS.getDefaultLanguage();
+                for (Locale locale : getAvailableLanguages()) {
+                    final Set<String> features = sTTS.getFeatures(locale);
+                    boolean isLocal = features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
+                    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.LOLLIPOP) {
+            return sTTS.getAvailableLanguages();
+        }
+        Set<Locale> locales = new HashSet<Locale>();
+        for (Locale locale : Locale.getAvailableLocales()) {
+            if (locale.getVariant().isEmpty() && sTTS.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, float rate, float pitch, float volume) {
+        HashMap<String, String> params = new HashMap<String, String>();
+        final String utteranceId = UUID.randomUUID().toString();
+        params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(volume));
+        params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
+        sTTS.setLanguage(new Locale(uri.substring("moz-tts:android:".length())));
+        sTTS.setSpeechRate(rate);
+        sTTS.setPitch(pitch);
+        int result = sTTS.speak(text, TextToSpeech.QUEUE_FLUSH, params);
+        if (result != TextToSpeech.SUCCESS) {
+            return null;
+        }
+
+        return utteranceId;
+    }
+
+    private static void setUtteranceListener() {
+        sTTS.setOnUtteranceProgressListener(new UtteranceProgressListener() {
+            @Override
+            public void onDone(String utteranceId) {
+                dispatchEnd(utteranceId);
+            }
+
+            @Override
+            public void onError(String utteranceId) {
+                dispatchError(utteranceId);
+            }
+
+            @Override
+            public void onStart(String utteranceId) {
+                dispatchStart(utteranceId);
+            }
+
+            @Override
+            public void onStop(String utteranceId, boolean interrupted) {
+                if (interrupted) {
+                    dispatchEnd(utteranceId);
+                } else {
+                    // utterance isn't started yet.
+                    dispatchError(utteranceId);
+                }
+            }
+
+            public void onRangeStart (String utteranceId, int start, int end, 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() {
+        sTTS.stop();
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            // Android M has onStop method.  If Android L or above, dispatch
+            // event
+            dispatchEnd(null);
+        }
+    }
+}
--- a/widget/android/GeneratedJNINatives.h
+++ b/widget/android/GeneratedJNINatives.h
@@ -322,16 +322,51 @@ const JNINativeMethod ScreenManagerHelpe
             ::template Wrap<&Impl::AddDisplay>),
 
     mozilla::jni::MakeNativeMethod<ScreenManagerHelper::RemoveDisplay_t>(
             mozilla::jni::NativeStub<ScreenManagerHelper::RemoveDisplay_t, Impl>
             ::template Wrap<&Impl::RemoveDisplay>)
 };
 
 template<class Impl>
+class SpeechSynthesisService::Natives : public mozilla::jni::NativeImpl<SpeechSynthesisService, Impl>
+{
+public:
+    static const JNINativeMethod methods[6];
+};
+
+template<class Impl>
+const JNINativeMethod SpeechSynthesisService::Natives<Impl>::methods[] = {
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::DispatchBoundary_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::DispatchBoundary_t, Impl>
+            ::template Wrap<&Impl::DispatchBoundary>),
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::DispatchEnd_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::DispatchEnd_t, Impl>
+            ::template Wrap<&Impl::DispatchEnd>),
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::DispatchError_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::DispatchError_t, Impl>
+            ::template Wrap<&Impl::DispatchError>),
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::DispatchStart_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::DispatchStart_t, Impl>
+            ::template Wrap<&Impl::DispatchStart>),
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::DoneRegisteringVoices_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::DoneRegisteringVoices_t, Impl>
+            ::template Wrap<&Impl::DoneRegisteringVoices>),
+
+    mozilla::jni::MakeNativeMethod<SpeechSynthesisService::RegisterVoice_t>(
+            mozilla::jni::NativeStub<SpeechSynthesisService::RegisterVoice_t, Impl>
+            ::template Wrap<&Impl::RegisterVoice>)
+};
+
+template<class Impl>
 class SurfaceTextureListener::Natives : public mozilla::jni::NativeImpl<SurfaceTextureListener, Impl>
 {
 public:
     static const JNINativeMethod methods[2];
 };
 
 template<class Impl>
 const JNINativeMethod SurfaceTextureListener::Natives<Impl>::methods[] = {
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -1012,16 +1012,61 @@ const char ScreenManagerHelper::name[] =
         "org/mozilla/gecko/ScreenManagerHelper";
 
 constexpr char ScreenManagerHelper::AddDisplay_t::name[];
 constexpr char ScreenManagerHelper::AddDisplay_t::signature[];
 
 constexpr char ScreenManagerHelper::RemoveDisplay_t::name[];
 constexpr char ScreenManagerHelper::RemoveDisplay_t::signature[];
 
+const char SpeechSynthesisService::name[] =
+        "org/mozilla/gecko/SpeechSynthesisService";
+
+constexpr char SpeechSynthesisService::DispatchBoundary_t::name[];
+constexpr char SpeechSynthesisService::DispatchBoundary_t::signature[];
+
+constexpr char SpeechSynthesisService::DispatchEnd_t::name[];
+constexpr char SpeechSynthesisService::DispatchEnd_t::signature[];
+
+constexpr char SpeechSynthesisService::DispatchError_t::name[];
+constexpr char SpeechSynthesisService::DispatchError_t::signature[];
+
+constexpr char SpeechSynthesisService::DispatchStart_t::name[];
+constexpr char SpeechSynthesisService::DispatchStart_t::signature[];
+
+constexpr char SpeechSynthesisService::DoneRegisteringVoices_t::name[];
+constexpr char SpeechSynthesisService::DoneRegisteringVoices_t::signature[];
+
+constexpr char SpeechSynthesisService::InitSynth_t::name[];
+constexpr char SpeechSynthesisService::InitSynth_t::signature[];
+
+auto SpeechSynthesisService::InitSynth() -> void
+{
+    return mozilla::jni::Method<InitSynth_t>::Call(SpeechSynthesisService::Context(), nullptr);
+}
+
+constexpr char SpeechSynthesisService::RegisterVoice_t::name[];
+constexpr char SpeechSynthesisService::RegisterVoice_t::signature[];
+
+constexpr char SpeechSynthesisService::Speak_t::name[];
+constexpr char SpeechSynthesisService::Speak_t::signature[];
+
+auto SpeechSynthesisService::Speak(mozilla::jni::String::Param a0, mozilla::jni::String::Param a1, float a2, float a3, float a4) -> mozilla::jni::String::LocalRef
+{
+    return mozilla::jni::Method<Speak_t>::Call(SpeechSynthesisService::Context(), nullptr, a0, a1, a2, a3, a4);
+}
+
+constexpr char SpeechSynthesisService::Stop_t::name[];
+constexpr char SpeechSynthesisService::Stop_t::signature[];
+
+auto SpeechSynthesisService::Stop() -> void
+{
+    return mozilla::jni::Method<Stop_t>::Call(SpeechSynthesisService::Context(), nullptr);
+}
+
 const char SurfaceTextureListener::name[] =
         "org/mozilla/gecko/SurfaceTextureListener";
 
 constexpr char SurfaceTextureListener::New_t::name[];
 constexpr char SurfaceTextureListener::New_t::signature[];
 
 auto SurfaceTextureListener::New() -> SurfaceTextureListener::LocalRef
 {
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -3090,16 +3090,204 @@ public:
     };
 
     static const mozilla::jni::CallingThread callingThread =
             mozilla::jni::CallingThread::ANY;
 
     template<class Impl> class Natives;
 };
 
+class SpeechSynthesisService : public mozilla::jni::ObjectBase<SpeechSynthesisService>
+{
+public:
+    static const char name[];
+
+    explicit SpeechSynthesisService(const Context& ctx) : ObjectBase<SpeechSynthesisService>(ctx) {}
+
+    struct DispatchBoundary_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param,
+                int32_t,
+                int32_t> Args;
+        static constexpr char name[] = "dispatchBoundary";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;II)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct DispatchEnd_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param> Args;
+        static constexpr char name[] = "dispatchEnd";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct DispatchError_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param> Args;
+        static constexpr char name[] = "dispatchError";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct DispatchStart_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param> Args;
+        static constexpr char name[] = "dispatchStart";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct DoneRegisteringVoices_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<> Args;
+        static constexpr char name[] = "doneRegisteringVoices";
+        static constexpr char signature[] =
+                "()V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct InitSynth_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<> Args;
+        static constexpr char name[] = "initSynth";
+        static constexpr char signature[] =
+                "()V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::GECKO;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::CURRENT;
+    };
+
+    static auto InitSynth() -> void;
+
+    struct RegisterVoice_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                bool,
+                bool> Args;
+        static constexpr char name[] = "registerVoice";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZ)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::ANY;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::GECKO;
+    };
+
+    struct Speak_t {
+        typedef SpeechSynthesisService Owner;
+        typedef mozilla::jni::String::LocalRef ReturnType;
+        typedef mozilla::jni::String::Param SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                float,
+                float,
+                float> Args;
+        static constexpr char name[] = "speak";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;Ljava/lang/String;FFF)Ljava/lang/String;";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::GECKO;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::CURRENT;
+    };
+
+    static auto Speak(mozilla::jni::String::Param, mozilla::jni::String::Param, float, float, float) -> mozilla::jni::String::LocalRef;
+
+    struct Stop_t {
+        typedef SpeechSynthesisService Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<> Args;
+        static constexpr char name[] = "stop";
+        static constexpr char signature[] =
+                "()V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+        static const mozilla::jni::CallingThread callingThread =
+                mozilla::jni::CallingThread::GECKO;
+        static const mozilla::jni::DispatchTarget dispatchTarget =
+                mozilla::jni::DispatchTarget::CURRENT;
+    };
+
+    static auto Stop() -> void;
+
+    static const mozilla::jni::CallingThread callingThread =
+            mozilla::jni::CallingThread::ANY;
+
+    template<class Impl> class Natives;
+};
+
 class SurfaceTextureListener : public mozilla::jni::ObjectBase<SurfaceTextureListener>
 {
 public:
     static const char name[];
 
     explicit SurfaceTextureListener(const Context& ctx) : ObjectBase<SurfaceTextureListener>(ctx) {}
 
     struct New_t {