Move Android's TTS platform implementation to content/

Bug: 517317
Change-Id: I9fc1edc58a2386b837b8677c4984ae4e5829c7e0
Reviewed-on: https://chromium-review.googlesource.com/c/1359453
Commit-Queue: Katie Dektar <[email protected]>
Reviewed-by: Dominic Mazzoni <[email protected]>
Reviewed-by: John Abd-El-Malek <[email protected]>
Cr-Commit-Position: refs/heads/master@{#613604}
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 64be0231..d2ac80b 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1701,6 +1701,8 @@
     "speech/speech_recognizer.h",
     "speech/speech_recognizer_impl_android.cc",
     "speech/speech_recognizer_impl_android.h",
+    "speech/tts_android.cc",
+    "speech/tts_android.h",
     "speech/tts_controller_impl.cc",
     "speech/tts_controller_impl.h",
     "speech/tts_mac.mm",
diff --git a/content/browser/speech/tts_android.cc b/content/browser/speech/tts_android.cc
new file mode 100644
index 0000000..dce7244
--- /dev/null
+++ b/content/browser/speech/tts_android.cc
@@ -0,0 +1,146 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/speech/tts_android.h"
+
+#include <string>
+
+#include "base/android/jni_string.h"
+#include "base/memory/singleton.h"
+#include "base/strings/utf_string_conversions.h"
+#include "content/common/buildflags.h"
+#include "content/public/browser/tts_controller.h"
+#include "jni/TtsPlatformImpl_jni.h"
+
+using base::android::AttachCurrentThread;
+using base::android::JavaParamRef;
+
+namespace content {
+
+TtsPlatformImplAndroid::TtsPlatformImplAndroid() : utterance_id_(0) {
+  JNIEnv* env = AttachCurrentThread();
+  java_ref_.Reset(
+      Java_TtsPlatformImpl_create(env, reinterpret_cast<intptr_t>(this)));
+}
+
+TtsPlatformImplAndroid::~TtsPlatformImplAndroid() {
+  JNIEnv* env = AttachCurrentThread();
+  Java_TtsPlatformImpl_destroy(env, java_ref_);
+}
+
+bool TtsPlatformImplAndroid::PlatformImplAvailable() {
+  return true;
+}
+
+bool TtsPlatformImplAndroid::Speak(
+    int utterance_id,
+    const std::string& utterance,
+    const std::string& lang,
+    const VoiceData& voice,
+    const UtteranceContinuousParameters& params) {
+  JNIEnv* env = AttachCurrentThread();
+  jboolean success = Java_TtsPlatformImpl_speak(
+      env, java_ref_, utterance_id,
+      base::android::ConvertUTF8ToJavaString(env, utterance),
+      base::android::ConvertUTF8ToJavaString(env, lang), params.rate,
+      params.pitch, params.volume);
+  if (!success)
+    return false;
+
+  utterance_ = utterance;
+  utterance_id_ = utterance_id;
+  return true;
+}
+
+bool TtsPlatformImplAndroid::StopSpeaking() {
+  JNIEnv* env = AttachCurrentThread();
+  Java_TtsPlatformImpl_stop(env, java_ref_);
+  utterance_id_ = 0;
+  utterance_.clear();
+  return true;
+}
+
+void TtsPlatformImplAndroid::Pause() {
+  StopSpeaking();
+}
+
+void TtsPlatformImplAndroid::Resume() {}
+
+bool TtsPlatformImplAndroid::IsSpeaking() {
+  return (utterance_id_ != 0);
+}
+
+void TtsPlatformImplAndroid::GetVoices(std::vector<VoiceData>* out_voices) {
+  JNIEnv* env = AttachCurrentThread();
+  if (!Java_TtsPlatformImpl_isInitialized(env, java_ref_))
+    return;
+
+  int count = Java_TtsPlatformImpl_getVoiceCount(env, java_ref_);
+  for (int i = 0; i < count; ++i) {
+    out_voices->push_back(VoiceData());
+    VoiceData& data = out_voices->back();
+    data.native = true;
+    data.name = base::android::ConvertJavaStringToUTF8(
+        Java_TtsPlatformImpl_getVoiceName(env, java_ref_, i));
+    data.lang = base::android::ConvertJavaStringToUTF8(
+        Java_TtsPlatformImpl_getVoiceLanguage(env, java_ref_, i));
+    data.events.insert(TTS_EVENT_START);
+    data.events.insert(TTS_EVENT_END);
+    data.events.insert(TTS_EVENT_ERROR);
+  }
+}
+
+void TtsPlatformImplAndroid::VoicesChanged(JNIEnv* env,
+                                           const JavaParamRef<jobject>& obj) {
+  TtsController::GetInstance()->VoicesChanged();
+}
+
+void TtsPlatformImplAndroid::OnEndEvent(JNIEnv* env,
+                                        const JavaParamRef<jobject>& obj,
+                                        jint utterance_id) {
+  SendFinalTtsEvent(utterance_id, TTS_EVENT_END,
+                    static_cast<int>(utterance_.size()));
+}
+
+void TtsPlatformImplAndroid::OnErrorEvent(JNIEnv* env,
+                                          const JavaParamRef<jobject>& obj,
+                                          jint utterance_id) {
+  SendFinalTtsEvent(utterance_id, TTS_EVENT_ERROR, 0);
+}
+
+void TtsPlatformImplAndroid::OnStartEvent(JNIEnv* env,
+                                          const JavaParamRef<jobject>& obj,
+                                          jint utterance_id) {
+  if (utterance_id != utterance_id_)
+    return;
+
+  TtsController::GetInstance()->OnTtsEvent(utterance_id_, TTS_EVENT_START, 0,
+                                           std::string());
+}
+
+void TtsPlatformImplAndroid::SendFinalTtsEvent(int utterance_id,
+                                               TtsEventType event_type,
+                                               int char_index) {
+  if (utterance_id != utterance_id_)
+    return;
+
+  TtsController::GetInstance()->OnTtsEvent(utterance_id_, event_type,
+                                           char_index, std::string());
+  utterance_id_ = 0;
+  utterance_.clear();
+}
+
+// static
+TtsPlatformImplAndroid* TtsPlatformImplAndroid::GetInstance() {
+  return base::Singleton<
+      TtsPlatformImplAndroid,
+      base::LeakySingletonTraits<TtsPlatformImplAndroid>>::get();
+}
+
+// static
+TtsPlatformImpl* TtsPlatformImpl::GetInstance() {
+  return TtsPlatformImplAndroid::GetInstance();
+}
+
+}  // namespace content
diff --git a/content/browser/speech/tts_android.h b/content/browser/speech/tts_android.h
new file mode 100644
index 0000000..da8dd04
--- /dev/null
+++ b/content/browser/speech/tts_android.h
@@ -0,0 +1,64 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_SPEECH_TTS_ANDROID_H_
+#define CONTENT_BROWSER_SPEECH_TTS_ANDROID_H_
+
+#include "base/android/scoped_java_ref.h"
+#include "base/macros.h"
+#include "content/browser/speech/tts_platform_impl.h"
+
+namespace content {
+
+class TtsPlatformImplAndroid : public TtsPlatformImpl {
+ public:
+  // TtsPlatform overrides.
+  bool PlatformImplAvailable() override;
+  bool Speak(int utterance_id,
+             const std::string& utterance,
+             const std::string& lang,
+             const VoiceData& voice,
+             const UtteranceContinuousParameters& params) override;
+  bool StopSpeaking() override;
+  void Pause() override;
+  void Resume() override;
+  bool IsSpeaking() override;
+  void GetVoices(std::vector<VoiceData>* out_voices) override;
+
+  // Methods called from Java via JNI.
+  void VoicesChanged(JNIEnv* env,
+                     const base::android::JavaParamRef<jobject>& obj);
+  void OnEndEvent(JNIEnv* env,
+                  const base::android::JavaParamRef<jobject>& obj,
+                  jint utterance_id);
+  void OnErrorEvent(JNIEnv* env,
+                    const base::android::JavaParamRef<jobject>& obj,
+                    jint utterance_id);
+  void OnStartEvent(JNIEnv* env,
+                    const base::android::JavaParamRef<jobject>& obj,
+                    jint utterance_id);
+
+  // Static functions.
+  static TtsPlatformImplAndroid* GetInstance();
+
+ private:
+  friend struct base::DefaultSingletonTraits<TtsPlatformImplAndroid>;
+
+  TtsPlatformImplAndroid();
+  ~TtsPlatformImplAndroid() override;
+
+  void SendFinalTtsEvent(int utterance_id,
+                         TtsEventType event_type,
+                         int char_index);
+
+  base::android::ScopedJavaGlobalRef<jobject> java_ref_;
+  int utterance_id_;
+  std::string utterance_;
+
+  DISALLOW_COPY_AND_ASSIGN(TtsPlatformImplAndroid);
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_SPEECH_TTS_ANDROID_H_
diff --git a/content/browser/speech/tts_platform_impl.cc b/content/browser/speech/tts_platform_impl.cc
index 77461545..0a9a1bd 100644
--- a/content/browser/speech/tts_platform_impl.cc
+++ b/content/browser/speech/tts_platform_impl.cc
@@ -13,8 +13,8 @@
 
 // static
 TtsPlatform* TtsPlatform::GetInstance() {
-#if defined(OS_CHROMEOS) || defined(OS_ANDROID)
-  // Chrome and Android TTS platforms have chrome/ dependencies.
+#if defined(OS_CHROMEOS)
+  // Chrome TTS platform has chrome/ dependencies.
   return GetContentClient()->browser()->GetTtsPlatform();
 #elif defined(OS_FUCHSIA)
   // There is no platform TTS definition for Fuchsia.
@@ -44,4 +44,4 @@
   error_ = error;
 }
 
-}  // namespace content
\ No newline at end of file
+}  // namespace content
diff --git a/content/public/android/BUILD.gn b/content/public/android/BUILD.gn
index 98da942..5d6b10a3 100644
--- a/content/public/android/BUILD.gn
+++ b/content/public/android/BUILD.gn
@@ -135,10 +135,12 @@
     "java/src/org/chromium/content/browser/JavascriptInterface.java",
     "java/src/org/chromium/content/browser/JoystickHandler.java",
     "java/src/org/chromium/content/browser/LauncherThread.java",
+    "java/src/org/chromium/content/browser/LollipopTtsPlatformImpl.java",
     "java/src/org/chromium/content/browser/MediaSessionImpl.java",
     "java/src/org/chromium/content/browser/MemoryMonitorAndroid.java",
     "java/src/org/chromium/content/browser/MotionEventSynthesizerImpl.java",
     "java/src/org/chromium/content/browser/NfcHost.java",
+    "java/src/org/chromium/content/browser/TtsPlatformImpl.java",
     "java/src/org/chromium/content/browser/PopupController.java",
     "java/src/org/chromium/content/browser/RenderCoordinatesImpl.java",
     "java/src/org/chromium/content/browser/ScreenOrientationProviderImpl.java",
@@ -383,6 +385,7 @@
     "java/src/org/chromium/content/browser/SpeechRecognitionImpl.java",
     "java/src/org/chromium/content/browser/SyntheticGestureTarget.java",
     "java/src/org/chromium/content/browser/TracingControllerAndroidImpl.java",
+    "java/src/org/chromium/content/browser/TtsPlatformImpl.java",
     "java/src/org/chromium/content/browser/accessibility/BrowserAccessibilityState.java",
     "java/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityImpl.java",
     "java/src/org/chromium/content/browser/accessibility/captioning/CaptioningController.java",
diff --git a/content/public/android/java/src/org/chromium/content/browser/LollipopTtsPlatformImpl.java b/content/public/android/java/src/org/chromium/content/browser/LollipopTtsPlatformImpl.java
new file mode 100644
index 0000000..6e4fc58
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/LollipopTtsPlatformImpl.java
@@ -0,0 +1,64 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.content.browser;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+
+import org.chromium.base.annotations.JNINamespace;
+
+/**
+ * Subclass of TtsPlatformImpl for Lollipop to make use of newer APIs.
+ */
+@JNINamespace("content")
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+class LollipopTtsPlatformImpl extends TtsPlatformImpl {
+    protected LollipopTtsPlatformImpl(long nativeTtsPlatformImplAndroid) {
+        super(nativeTtsPlatformImplAndroid);
+    }
+
+    /**
+     * Overrides TtsPlatformImpl because the API changed in Lollipop.
+     */
+    @Override
+    protected void addOnUtteranceProgressListener() {
+        mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
+            @Override
+            public void onDone(final String utteranceId) {
+                sendEndEventOnUiThread(utteranceId);
+            }
+
+            @Override
+            public void onError(final String utteranceId, int errorCode) {
+                sendErrorEventOnUiThread(utteranceId);
+            }
+
+            @Override
+            @Deprecated
+            public void onError(final String utteranceId) {}
+
+            @Override
+            public void onStart(final String utteranceId) {
+                sendStartEventOnUiThread(utteranceId);
+            }
+        });
+    }
+
+    /**
+     * Overrides TtsPlatformImpl because the API changed in Lollipop.
+     */
+    @Override
+    protected int callSpeak(String text, float volume, int utteranceId) {
+        Bundle params = new Bundle();
+        if (volume != 1.0) {
+            params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume);
+        }
+        return mTextToSpeech.speak(
+                text, TextToSpeech.QUEUE_FLUSH, params, Integer.toString(utteranceId));
+    }
+}
diff --git a/content/public/android/java/src/org/chromium/content/browser/TtsPlatformImpl.java b/content/public/android/java/src/org/chromium/content/browser/TtsPlatformImpl.java
new file mode 100644
index 0000000..d7bf9d5
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/TtsPlatformImpl.java
@@ -0,0 +1,319 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.content.browser;
+
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.TraceEvent;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.task.AsyncTask;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * This class is the Java counterpart to the C++ TtsPlatformImplAndroid class.
+ * It implements the Android-native text-to-speech code to support the web
+ * speech synthesis API.
+ *
+ * Threading model note: all calls from C++ must happen on the UI thread.
+ * Callbacks from Android may happen on a different thread, so we always
+ * use ThreadUtils.runOnUiThread when calling back to C++.
+ */
+@JNINamespace("content")
+class TtsPlatformImpl {
+    private static class TtsVoice {
+        private TtsVoice(String name, String language) {
+            mName = name;
+            mLanguage = language;
+        }
+        private final String mName;
+        private final String mLanguage;
+    }
+
+    private static class PendingUtterance {
+        private PendingUtterance(TtsPlatformImpl impl, int utteranceId, String text, String lang,
+                float rate, float pitch, float volume) {
+            mImpl = impl;
+            mUtteranceId = utteranceId;
+            mText = text;
+            mLang = lang;
+            mRate = rate;
+            mPitch = pitch;
+            mVolume = volume;
+        }
+
+        private void speak() {
+            mImpl.speak(mUtteranceId, mText, mLang, mRate, mPitch, mVolume);
+        }
+
+        TtsPlatformImpl mImpl;
+        int mUtteranceId;
+        String mText;
+        String mLang;
+        float mRate;
+        float mPitch;
+        float mVolume;
+    }
+
+    private long mNativeTtsPlatformImplAndroid;
+    protected final TextToSpeech mTextToSpeech;
+    private boolean mInitialized;
+    private List<TtsVoice> mVoices;
+    private String mCurrentLanguage;
+    private PendingUtterance mPendingUtterance;
+
+    protected TtsPlatformImpl(long nativeTtsPlatformImplAndroid) {
+        mInitialized = false;
+        mNativeTtsPlatformImplAndroid = nativeTtsPlatformImplAndroid;
+        mTextToSpeech = new TextToSpeech(ContextUtils.getApplicationContext(), status -> {
+            if (status == TextToSpeech.SUCCESS) {
+                ThreadUtils.runOnUiThread(() -> initialize());
+            }
+        });
+        addOnUtteranceProgressListener();
+    }
+
+    /**
+     * Create a TtsPlatformImpl object, which is owned by TtsPlatformImplAndroid
+     * on the C++ side.
+     *  @param nativeTtsPlatformImplAndroid The C++ object that owns us.
+     *
+     */
+    @CalledByNative
+    private static TtsPlatformImpl create(long nativeTtsPlatformImplAndroid) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return new LollipopTtsPlatformImpl(nativeTtsPlatformImplAndroid);
+        } else {
+            return new TtsPlatformImpl(nativeTtsPlatformImplAndroid);
+        }
+    }
+
+    /**
+     * Called when our C++ counterpoint is deleted. Clear the handle to our
+     * native C++ object, ensuring it's never called.
+     */
+    @CalledByNative
+    private void destroy() {
+        mNativeTtsPlatformImplAndroid = 0;
+    }
+
+    /**
+     * @return true if our TextToSpeech object is initialized and we've
+     * finished scanning the list of voices.
+     */
+    @CalledByNative
+    private boolean isInitialized() {
+        return mInitialized;
+    }
+
+    /**
+     * @return the number of voices.
+     */
+    @CalledByNative
+    private int getVoiceCount() {
+        assert mInitialized;
+        return mVoices.size();
+    }
+
+    /**
+     * @return the name of the voice at a given index.
+     */
+    @CalledByNative
+    private String getVoiceName(int voiceIndex) {
+        assert mInitialized;
+        return mVoices.get(voiceIndex).mName;
+    }
+
+    /**
+     * @return the language of the voice at a given index.
+     */
+    @CalledByNative
+    private String getVoiceLanguage(int voiceIndex) {
+        assert mInitialized;
+        return mVoices.get(voiceIndex).mLanguage;
+    }
+
+    /**
+     * Attempt to start speaking an utterance. If it returns true, will call back on
+     * start and end.
+     *
+     * @param utteranceId A unique id for this utterance so that callbacks can be tied
+     *     to a particular utterance.
+     * @param text The text to speak.
+     * @param lang The language code for the text (e.g., "en-US").
+     * @param rate The speech rate, in the units expected by Android TextToSpeech.
+     * @param pitch The speech pitch, in the units expected by Android TextToSpeech.
+     * @param volume The speech volume, in the units expected by Android TextToSpeech.
+     * @return true on success.
+     */
+    @CalledByNative
+    private boolean speak(
+            int utteranceId, String text, String lang, float rate, float pitch, float volume) {
+        if (!mInitialized) {
+            mPendingUtterance =
+                    new PendingUtterance(this, utteranceId, text, lang, rate, pitch, volume);
+            return true;
+        }
+        if (mPendingUtterance != null) mPendingUtterance = null;
+
+        if (!lang.equals(mCurrentLanguage)) {
+            mTextToSpeech.setLanguage(new Locale(lang));
+            mCurrentLanguage = lang;
+        }
+
+        mTextToSpeech.setSpeechRate(rate);
+        mTextToSpeech.setPitch(pitch);
+
+        int result = callSpeak(text, volume, utteranceId);
+        return (result == TextToSpeech.SUCCESS);
+    }
+
+    /**
+     * Stop the current utterance.
+     */
+    @CalledByNative
+    private void stop() {
+        if (mInitialized) mTextToSpeech.stop();
+        if (mPendingUtterance != null) mPendingUtterance = null;
+    }
+
+    /**
+     * Post a task to the UI thread to send the TTS "end" event.
+     */
+    protected void sendEndEventOnUiThread(final String utteranceId) {
+        ThreadUtils.runOnUiThread(() -> {
+            if (mNativeTtsPlatformImplAndroid != 0) {
+                nativeOnEndEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId));
+            }
+        });
+    }
+
+    /**
+     * Post a task to the UI thread to send the TTS "error" event.
+     */
+    protected void sendErrorEventOnUiThread(final String utteranceId) {
+        ThreadUtils.runOnUiThread(() -> {
+            if (mNativeTtsPlatformImplAndroid != 0) {
+                nativeOnErrorEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId));
+            }
+        });
+    }
+
+    /**
+     * Post a task to the UI thread to send the TTS "start" event.
+     */
+    protected void sendStartEventOnUiThread(final String utteranceId) {
+        ThreadUtils.runOnUiThread(() -> {
+            if (mNativeTtsPlatformImplAndroid != 0) {
+                nativeOnStartEvent(mNativeTtsPlatformImplAndroid, Integer.parseInt(utteranceId));
+            }
+        });
+    }
+
+    /**
+     * This is overridden by LollipopTtsPlatformImpl because the API changed.
+     */
+    @SuppressWarnings("deprecation")
+    protected void addOnUtteranceProgressListener() {
+        mTextToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
+            @Override
+            public void onDone(final String utteranceId) {
+                sendEndEventOnUiThread(utteranceId);
+            }
+
+            // This is deprecated in Lollipop and higher but we still need to catch it
+            // on pre-Lollipop builds.
+            @Override
+            @SuppressWarnings("deprecation")
+            public void onError(final String utteranceId) {
+                sendErrorEventOnUiThread(utteranceId);
+            }
+
+            @Override
+            public void onStart(final String utteranceId) {
+                sendStartEventOnUiThread(utteranceId);
+            }
+        });
+    }
+
+    /**
+     * This is overridden by LollipopTtsPlatformImpl because the API changed.
+     */
+    @SuppressWarnings("deprecation")
+    protected int callSpeak(String text, float volume, int utteranceId) {
+        HashMap<String, String> params = new HashMap<String, String>();
+        if (volume != 1.0) {
+            params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Double.toString(volume));
+        }
+        params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(utteranceId));
+        return mTextToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params);
+    }
+
+    /**
+     * Note: we enforce that this method is called on the UI thread, so
+     * we can call nativeVoicesChanged directly.
+     */
+    private void initialize() {
+        TraceEvent.begin("TtsPlatformImpl:initialize");
+
+        new AsyncTask<List<TtsVoice>>() {
+            @Override
+            protected List<TtsVoice> doInBackground() {
+                assert mNativeTtsPlatformImplAndroid != 0;
+
+                try (TraceEvent te = TraceEvent.scoped("TtsPlatformImpl:initialize.async_task")) {
+                    Locale[] locales = Locale.getAvailableLocales();
+                    final List<TtsVoice> voices = new ArrayList<>();
+                    for (Locale locale : locales) {
+                        if (!locale.getVariant().isEmpty()) continue;
+                        try {
+                            if (mTextToSpeech.isLanguageAvailable(locale) > 0) {
+                                String name = locale.getDisplayLanguage();
+                                if (!locale.getCountry().isEmpty()) {
+                                    name += " " + locale.getDisplayCountry();
+                                }
+                                TtsVoice voice = new TtsVoice(name, locale.toString());
+                                voices.add(voice);
+                            }
+                        } catch (Exception e) {
+                            // Just skip the locale if it's invalid.
+                            //
+                            // We used to catch only java.util.MissingResourceException,
+                            // but we need to catch more exceptions to work around a bug
+                            // in Google TTS when we query "bn".
+                            // http://crbug.com/792856
+                        }
+                    }
+                    return voices;
+                }
+            }
+
+            @Override
+            protected void onPostExecute(List<TtsVoice> voices) {
+                mVoices = voices;
+                mInitialized = true;
+
+                nativeVoicesChanged(mNativeTtsPlatformImplAndroid);
+
+                if (mPendingUtterance != null) mPendingUtterance.speak();
+
+                TraceEvent.end("TtsPlatformImpl:initialize");
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    private native void nativeVoicesChanged(long nativeTtsPlatformImplAndroid);
+    private native void nativeOnEndEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
+    private native void nativeOnStartEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
+    private native void nativeOnErrorEvent(long nativeTtsPlatformImplAndroid, int utteranceId);
+}