/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.messenger; import android.content.Context; import android.os.Handler; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.util.Log; import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; /** * Component that wraps platform TTS engine and supports queued playout. *

* It takes care of initializing the TTS engine. TTS requests made are queued up and played when the * engine is setup. It only supports one queued requests; any new requests will cause the existing * one to be dropped. Similarly, if a new one is queued while an existing message is already playing * the existing one will be stopped/interrupted and the new one will start playing. */ class TTSHelper { interface Listener { // Called when playout is about to start. void onTTSStarted(); // The following two are terminal callbacks and no further callbacks should be expected. // Called when playout finishes or playout is cancelled/never started because another TTS // request was made. void onTTSStopped(); // Called when there's an internal error. void onTTSError(); } private static final String TAG = "Messenger.TTSHelper"; private static final boolean DBG = MessengerService.DBG; private final Handler mHandler = new Handler(); private final TextToSpeech mTextToSpeech; private int mInitStatus; private SpeechRequest mPendingRequest; private final Map mListeners = new HashMap<>(); TTSHelper(Context context) { // OnInitListener will only set to SUCCESS/ERROR. So we initialize to STOPPED. mInitStatus = TextToSpeech.STOPPED; // TODO(sriniv): Init this only when needed and shutdown to free resources. mTextToSpeech = new TextToSpeech(context, this::handleInitCompleted); mTextToSpeech.setOnUtteranceProgressListener(mProgressListener); } private void handleInitCompleted(int initStatus) { if (DBG) { Log.d(TAG, "init completed: " + initStatus); } mInitStatus = initStatus; if (mPendingRequest != null) { playInternal(mPendingRequest.mTextToSpeak, mPendingRequest.mListener); mPendingRequest = null; } } void requestPlay(CharSequence textToSpeak, Listener listener) { // Check if its still initializing. if (mInitStatus == TextToSpeech.STOPPED) { // Squash any already queued request. if (mPendingRequest != null) { mPendingRequest.mListener.onTTSStopped(); } mPendingRequest = new SpeechRequest(textToSpeak, listener); } else { playInternal(textToSpeak, listener); } } void requestStop() { mTextToSpeech.stop(); } private void playInternal(CharSequence textToSpeak, Listener listener) { if (mInitStatus == TextToSpeech.ERROR) { Log.e(TAG, "TTS setup failed!"); mHandler.post(listener::onTTSError); return; } String id = Integer.toString(listener.hashCode()); if (DBG) { Log.d(TAG, String.format("Queueing text in TTS: [%s], id=%s", textToSpeak, id)); } if (mTextToSpeech.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, id) != TextToSpeech.SUCCESS) { Log.e(TAG, "Queuing text failed!"); mHandler.post(listener::onTTSError); return; } mListeners.put(id, listener); } void cleanup() { mTextToSpeech.stop(); mTextToSpeech.shutdown(); } // The TTS engine will invoke onStart and then invoke either onDone, onStop or onError. // Since these callbacks can come on other threads, we push updates back on to the TTSHelper's // Handler. private final UtteranceProgressListener mProgressListener = new UtteranceProgressListener() { private void safeInvokeAsync(String id, boolean cleanup, Consumer callbackCaller) { mHandler.post(() -> { Listener listener = mListeners.get(id); if (listener == null) { Log.e(TAG, "No listener found for: " + id); return; } callbackCaller.accept(listener); if (cleanup) { mListeners.remove(id); } }); } @Override public void onStart(String id) { if (DBG) { Log.d(TAG, "TTS engine onStart: " + id); } safeInvokeAsync(id, false, Listener::onTTSStarted); } @Override public void onDone(String id) { if (DBG) { Log.d(TAG, "TTS engine onDone: " + id); } safeInvokeAsync(id, true, Listener::onTTSStopped); } @Override public void onStop(String id, boolean interrupted) { if (DBG) { Log.d(TAG, "TTS engine onStop: " + id); } safeInvokeAsync(id, true, Listener::onTTSStopped); } @Override public void onError(String id) { if (DBG) { Log.d(TAG, "TTS engine onError: " + id); } safeInvokeAsync(id, true, Listener::onTTSError); } }; private static class SpeechRequest { final CharSequence mTextToSpeak; final Listener mListener; public SpeechRequest(CharSequence textToSpeak, Listener listener) { mTextToSpeak = textToSpeak; mListener = listener; } }; }