/****************************************************************************************
* Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com> *
* *
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 3 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/
package com.ichi2.anki;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.speech.tts.TextToSpeech;
import android.view.View;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.ichi2.compat.CompatHelper;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import timber.log.Timber;
public class ReadText {
private static TextToSpeech mTts;
private static ArrayList<Locale> availableTtsLocales = new ArrayList<>();
private static String mTextToSpeak;
private static WeakReference<Context> mReviewer;
private static long mDid;
private static int mOrd;
private static int mQuestionAnswer;
public static final String NO_TTS = "0";
public static ArrayList<String[]> sTextQueue = new ArrayList<>();
public static HashMap<String, String> mTtsParams;
// private boolean mTtsReady = false;
public static void speak(String text, String loc) {
int result = mTts.setLanguage(new Locale(loc));
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
Toast.makeText(mReviewer.get(), mReviewer.get().getString(R.string.no_tts_available_message)
+" ("+loc+")", Toast.LENGTH_LONG).show();
Timber.e("Error loading locale " + loc);
} else {
if (mTts.isSpeaking()) {
Timber.d("tts engine appears to be busy... clearing queue");
stopTts();
//sTextQueue.add(new String[] { text, loc });
}
Timber.d("tts text '%s' to be played for locale (%s)",text, loc);
mTts.speak(mTextToSpeak, TextToSpeech.QUEUE_FLUSH, mTtsParams);
}
}
public static String getLanguage(long did, int ord, int qa) {
return MetaDB.getLanguage(mReviewer.get(), did, ord, qa);
}
/**
* Ask the user what language they want.
*
* @param text The text to be read
* @param did The deck id
* @param ord The card template ordinal
* @param qa The card question or card answer
*/
public static void selectTts(String text, long did, int ord, int qa) {
mTextToSpeak = text;
mQuestionAnswer = qa;
mDid = did;
mOrd = ord;
Resources res = mReviewer.get().getResources();
final MaterialDialog.Builder builder = new MaterialDialog.Builder(mReviewer.get());
// Build the language list if it's empty
if (availableTtsLocales.isEmpty()) {
buildAvailableLanguages();
}
if (availableTtsLocales.size() == 0) {
Timber.w("ReadText.textToSpeech() no TTS languages available");
builder.content(res.getString(R.string.no_tts_available_message))
.iconAttr(R.attr.dialogErrorIcon)
.positiveText(res.getString(R.string.dialog_ok));
} else {
ArrayList<CharSequence> dialogItems = new ArrayList<>();
final ArrayList<String> dialogIds = new ArrayList<>();
// Add option: "no tts"
dialogItems.add(res.getString(R.string.tts_no_tts));
dialogIds.add(NO_TTS);
for (int i = 0; i < availableTtsLocales.size(); i++) {
dialogItems.add(availableTtsLocales.get(i).getDisplayName());
dialogIds.add(availableTtsLocales.get(i).getISO3Language());
}
String[] items = new String[dialogItems.size()];
dialogItems.toArray(items);
builder.title(res.getString(R.string.select_locale_title))
.items(items)
.itemsCallback(new MaterialDialog.ListCallback() {
@Override
public void onSelection(MaterialDialog materialDialog, View view, int which,
CharSequence charSequence) {
String locale = dialogIds.get(which);
Timber.d("ReadText.selectTts() user chose locale '%s'", locale);
if (!locale.equals(NO_TTS)) {
speak(mTextToSpeak, locale);
}
String language = getLanguage(mDid, mOrd, mQuestionAnswer);
if (language.equals("")) { // No language stored
MetaDB.storeLanguage(mReviewer.get(), mDid, mOrd, mQuestionAnswer, locale);
} else {
MetaDB.updateLanguage(mReviewer.get(), mDid, mOrd, mQuestionAnswer, locale);
}
}
});
}
// Show the dialog after short delay so that user gets a chance to preview the card
final Handler handler = new Handler();
final int delay = 500;
handler.postDelayed(new Runnable() {
@Override
public void run() {
builder.build().show();
}
}, delay);
}
public static void textToSpeech(String text, long did, int ord, int qa) {
mTextToSpeak = text;
mQuestionAnswer = qa;
mDid = did;
mOrd = ord;
Timber.d("ReadText.textToSpeech() method started for string '%s'", text);
// get the user's existing language preference
String language = getLanguage(mDid, mOrd, mQuestionAnswer);
Timber.d("ReadText.textToSpeech() method found language choice '%s'", language);
// rebuild the language list if it's empty
if (availableTtsLocales.isEmpty()) {
buildAvailableLanguages();
}
// Check, if stored language is available
for (int i = 0; i < availableTtsLocales.size(); i++) {
if (language.equals(NO_TTS)) {
// user has chosen not to read the text
return;
} else if (language.equals(availableTtsLocales.get(i).getISO3Language())) {
speak(mTextToSpeak, language);
return;
}
}
// Otherwise ask the user what language they want to use
selectTts(mTextToSpeak, mDid, mOrd, mQuestionAnswer);
}
public static void initializeTts(Context context) {
// Store weak reference to Activity to prevent memory leak
mReviewer = new WeakReference<>(context);
// Create new TTS object and setup its onInit Listener
mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
// build list of available languages
buildAvailableLanguages();
if (availableTtsLocales.size() > 0) {
// notify the reviewer that TTS has been initialized
Timber.d("TTS initialized and available languages found");
((AbstractFlashcardViewer) mReviewer.get()).ttsInitialized();
} else {
Toast.makeText(mReviewer.get(), mReviewer.get().getString(R.string.no_tts_available_message), Toast.LENGTH_LONG).show();
Timber.w("TTS initialized but no available languages found");
}
} else {
Toast.makeText(mReviewer.get(), mReviewer.get().getString(R.string.no_tts_available_message), Toast.LENGTH_LONG).show();
}
CompatHelper.getCompat().setTtsOnUtteranceProgressListener(mTts);
}
});
mTtsParams = new HashMap<>();
mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "stringId");
// Show toast that it's getting initialized, as it can take a while before the sound plays the first time
Toast.makeText(context, context.getString(R.string.initializing_tts), Toast.LENGTH_LONG).show();
}
public static void buildAvailableLanguages() {
availableTtsLocales.clear();
Locale[] systemLocales = Locale.getAvailableLocales();
for (Locale loc : systemLocales) {
try {
int retCode = mTts.isLanguageAvailable(loc);
if (retCode >= TextToSpeech.LANG_COUNTRY_AVAILABLE) {
availableTtsLocales.add(loc);
} else {
Timber.v("ReadText.buildAvailableLanguages() :: %s not available (error code %d)", loc.getDisplayName(), retCode);
}
} catch (IllegalArgumentException e) {
Timber.e("Error checking if language " + loc.getDisplayName() + " available");
}
}
}
public static void releaseTts() {
if (mTts != null) {
mTts.stop();
mTts.shutdown();
}
}
public static void stopTts() {
if (mTts != null) {
if (sTextQueue != null) {
sTextQueue.clear();
}
mTts.stop();
}
}
}