diff --git a/app/src/main/java/com/bibliadiaria/app/MainActivity.java b/app/src/main/java/com/bibliadiaria/app/MainActivity.java index 369c881..3a4b78a 100644 --- a/app/src/main/java/com/bibliadiaria/app/MainActivity.java +++ b/app/src/main/java/com/bibliadiaria/app/MainActivity.java @@ -86,8 +86,9 @@ public class MainActivity extends Activity { private static final int RANK_FEAST = 2; private static final int RANK_SOLEMNITY = 3; - private static final String TTS_SPEECH_URL = "https://ttsapi.host2.gleeze.com/v1/audio/speech"; - private static final String TTS_MODEL = "kokoro"; + private static final String TTS_SPEECH_URL_GLEEZE = "https://ttsapi.host2.gleeze.com/v1/audio/speech"; + private static final String TTS_MODEL_GLEEZE = "kokoro"; + private static final String TTS_SPEECH_URL_MISTRAL = "https://api.mistral.ai/v1/text-to-speech"; private static final String READING_CACHE_DIR = "reading-cache"; private static final String PREFETCH_MONTH_PREFIX = "prefetch_month_"; private static final int NEXT_MONTH_PREFETCH_DAY = 26; @@ -1490,7 +1491,16 @@ public class MainActivity extends Activity { } private File downloadTtsAudio(String text) throws Exception { - HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL).openConnection(); + String provider = UpdateManager.getTtsProvider(this); + if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL)) { + return downloadMistralTtsAudio(text); + } else { + return downloadGleezTtsAudio(text); + } + } + + private File downloadGleezTtsAudio(String text) throws Exception { + HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL_GLEEZE).openConnection(); connection.setConnectTimeout(15000); connection.setReadTimeout(120000); connection.setRequestMethod("POST"); @@ -1500,8 +1510,8 @@ public class MainActivity extends Activity { connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); JSONObject body = new JSONObject(); - body.put("model", TTS_MODEL); - body.put("voice", UpdateManager.getTtsVoice(this, isEnglish)); + body.put("model", TTS_MODEL_GLEEZE); + body.put("voice", UpdateManager.getTtsVoice(this, isEnglish, UpdateManager.TTS_PROVIDER_GLEEZE)); body.put("input", text); body.put("response_format", "mp3"); body.put("speed", 1.0); @@ -1539,6 +1549,59 @@ public class MainActivity extends Activity { return outputFile; } + private File downloadMistralTtsAudio(String text) throws Exception { + String apiKey = UpdateManager.getMistralApiKey(this); + if (apiKey == null || apiKey.trim().isEmpty()) { + throw new IOException(isEnglish ? "Mistral API key is not configured. Please set it in Settings." : "La clave API de Mistral no está configurada. Configúrela en Ajustes."); + } + + HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL_MISTRAL).openConnection(); + connection.setConnectTimeout(15000); + connection.setReadTimeout(120000); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setRequestProperty("User-Agent", "BibliaDiaria/1.0 Android"); + connection.setRequestProperty("Accept", "audio/mpeg"); + connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + connection.setRequestProperty("Authorization", "Bearer " + apiKey); + + JSONObject body = new JSONObject(); + body.put("text", text); + body.put("voice", UpdateManager.getTtsVoice(this, isEnglish, UpdateManager.TTS_PROVIDER_MISTRAL)); + + byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(payload.length); + try (OutputStream stream = connection.getOutputStream()) { + stream.write(payload); + } + + int responseCode = connection.getResponseCode(); + if (responseCode < 200 || responseCode >= 300) { + String details = ""; + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + details = readStream(errorStream); + } + connection.disconnect(); + throw new IOException((isEnglish ? "Mistral TTS API responded with code " : "La API TTS de Mistral respondió con código ") + + responseCode + + (details.isEmpty() ? "." : ". " + details)); + } + + File outputFile = File.createTempFile("daily-reading-tts-", ".mp3", getCacheDir()); + try (InputStream input = connection.getInputStream(); + FileOutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } finally { + connection.disconnect(); + } + return outputFile; + } + private void showTtsAudioReady(File audioFile) { try { releaseTtsPlayer(); diff --git a/app/src/main/java/com/bibliadiaria/app/SettingsActivity.java b/app/src/main/java/com/bibliadiaria/app/SettingsActivity.java index fbf97be..2c0d241 100644 --- a/app/src/main/java/com/bibliadiaria/app/SettingsActivity.java +++ b/app/src/main/java/com/bibliadiaria/app/SettingsActivity.java @@ -7,9 +7,11 @@ import android.graphics.Typeface; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Bundle; +import android.text.InputType; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ScrollView; @@ -45,6 +47,8 @@ public class SettingsActivity extends Activity { private TextView downloadButton; private TextView voiceSubtitleText; private TextView voiceButton; + private TextView ttsProviderButton; + private TextView mistralApiKeyButton; private UpdateManager.UpdateInfo latestInfo; private boolean isEnglish; @@ -100,7 +104,9 @@ public class SettingsActivity extends Activity { content.addView(title, titleParams); content.addView(createLanguageCard()); + content.addView(createTtsProviderCard()); content.addView(createVoiceCard()); + content.addView(createMistralApiKeyCard()); content.addView(createVersionCard()); checkButton = actionButton(isEnglish ? "Check now" : "Comprobar", COLOR_ACCENT, Color.WHITE); @@ -200,6 +206,8 @@ public class SettingsActivity extends Activity { boolean current = UpdateManager.isEnglish(this); UpdateManager.setEnglish(this, !current); langToggle.setText(!current ? "English" : "Español"); + isEnglish = !current; + recreate(); updateVoiceCard(); }); card.addView(langToggle, new LinearLayout.LayoutParams(dp(100), dp(40))); @@ -207,6 +215,39 @@ public class SettingsActivity extends Activity { return card; } + private View createTtsProviderCard() { + LinearLayout card = createCard(); + card.setOrientation(LinearLayout.HORIZONTAL); + card.setGravity(Gravity.CENTER_VERTICAL); + + LinearLayout copy = new LinearLayout(this); + copy.setOrientation(LinearLayout.VERTICAL); + + TextView title = textView("TTS Provider", 20, ink(), Typeface.BOLD); + title.setIncludeFontPadding(false); + copy.addView(title); + + TextView subtitle = textView("Choose between Gleez and Mistral TTS", 14, muted(), Typeface.NORMAL); + LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + subtitleParams.setMargins(0, dp(6), 0, 0); + copy.addView(subtitle, subtitleParams); + + card.addView(copy, new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + )); + + ttsProviderButton = actionButton(getTtsProviderLabel(), COLOR_ACCENT, Color.WHITE); + ttsProviderButton.setOnClickListener(view -> showTtsProviderPicker()); + card.addView(ttsProviderButton, new LinearLayout.LayoutParams(dp(140), dp(40))); + + return card; + } + private View createVoiceCard() { LinearLayout card = createCard(); card.setOrientation(LinearLayout.VERTICAL); @@ -244,6 +285,42 @@ public class SettingsActivity extends Activity { return card; } + private View createMistralApiKeyCard() { + LinearLayout card = createCard(); + card.setOrientation(LinearLayout.VERTICAL); + + LinearLayout copy = new LinearLayout(this); + copy.setOrientation(LinearLayout.VERTICAL); + + TextView title = textView("Mistral API Key", 20, ink(), Typeface.BOLD); + title.setIncludeFontPadding(false); + copy.addView(title); + + TextView subtitle = textView("Required for Mistral TTS (get from mistral.ai)", 14, muted(), Typeface.NORMAL); + LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + subtitleParams.setMargins(0, dp(6), 0, 0); + copy.addView(subtitle, subtitleParams); + + card.addView(copy, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + )); + + mistralApiKeyButton = actionButton(getMistralApiKeyLabel(), COLOR_ACCENT, Color.WHITE); + mistralApiKeyButton.setOnClickListener(view -> showMistralApiKeyDialog()); + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + dp(42) + ); + buttonParams.setMargins(0, dp(12), 0, 0); + card.addView(mistralApiKeyButton, buttonParams); + + return card; + } + private View createVersionCard() { LinearLayout card = createCard(); card.setOrientation(LinearLayout.VERTICAL); @@ -319,7 +396,8 @@ public class SettingsActivity extends Activity { private void showVoicePicker() { boolean english = UpdateManager.isEnglish(this); - UpdateManager.VoiceOption[] options = UpdateManager.getTtsVoiceOptions(english); + String provider = UpdateManager.getTtsProvider(this); + UpdateManager.VoiceOption[] options = UpdateManager.getTtsVoiceOptions(english, provider); String[] labels = new String[options.length]; for (int index = 0; index < options.length; index++) { labels[index] = options[index].label; @@ -327,13 +405,14 @@ public class SettingsActivity extends Activity { int selectedIndex = UpdateManager.getTtsVoiceIndex( english, - UpdateManager.getTtsVoice(this, english) + provider, + UpdateManager.getTtsVoice(this, english, provider) ); new AlertDialog.Builder(this) .setTitle(english ? "English voice" : "Spanish voice") .setSingleChoiceItems(labels, selectedIndex, (dialog, which) -> { - UpdateManager.setTtsVoice(this, english, options[which].id); + UpdateManager.setTtsVoice(this, english, provider, options[which].id); updateVoiceCard(); dialog.dismiss(); }) @@ -341,17 +420,94 @@ public class SettingsActivity extends Activity { .show(); } + private void showTtsProviderPicker() { + String[] providers = {"Gleez", "Mistral"}; + String currentProvider = UpdateManager.getTtsProvider(this); + int selectedIndex = currentProvider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) ? 1 : 0; + + new AlertDialog.Builder(this) + .setTitle("Select TTS Provider") + .setSingleChoiceItems(providers, selectedIndex, (dialog, which) -> { + String provider = which == 1 ? UpdateManager.TTS_PROVIDER_MISTRAL : UpdateManager.TTS_PROVIDER_GLEEZE; + UpdateManager.setTtsProvider(this, provider); + ttsProviderButton.setText(getTtsProviderLabel()); + updateVoiceCard(); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showMistralApiKeyDialog() { + String currentKey = UpdateManager.getMistralApiKey(this); + String displayKey = currentKey.isEmpty() ? "" : "••••••••"; + + EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + input.setHint("Enter your Mistral API key"); + input.setText(currentKey); + input.setSelection(currentKey.length()); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(dp(20), dp(20), dp(20), dp(20)); + layout.addView(input, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + )); + + new AlertDialog.Builder(this) + .setTitle("Mistral API Key") + .setView(layout) + .setPositiveButton("Save", (dialog, which) -> { + UpdateManager.setMistralApiKey(this, input.getText().toString().trim()); + mistralApiKeyButton.setText(getMistralApiKeyLabel()); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private String getTtsProviderLabel() { + String provider = UpdateManager.getTtsProvider(this); + return provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) ? "Mistral" : "Gleez"; + } + + private String getMistralApiKeyLabel() { + String apiKey = UpdateManager.getMistralApiKey(this); + if (apiKey.isEmpty()) { + return "Not configured"; + } + return "••••••••"; + } + private void updateVoiceCard() { if (voiceSubtitleText == null || voiceButton == null) { return; } boolean english = UpdateManager.isEnglish(this); + String provider = UpdateManager.getTtsProvider(this); String voiceId = UpdateManager.getTtsVoice(this, english); - voiceSubtitleText.setText(english - ? "English voices are shown while English is selected" - : "Las voces en español se muestran mientras el español esté seleccionado"); - voiceButton.setText(UpdateManager.getTtsVoiceLabel(english, voiceId)); + + // Update subtitle based on provider + if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL)) { + voiceSubtitleText.setText(english + ? "Mistral voices (requires API key)" + : "Voces de Mistral (requiere clave API)"); + } else { + voiceSubtitleText.setText(english + ? "Gleez voices are shown while English is selected" + : "Las voces de Gleez se muestran mientras el idioma esta seleccionado"); + } + + voiceButton.setText(UpdateManager.getTtsVoiceLabel(english, provider, voiceId)); + + // If Mistral is selected but API key is not configured, show a warning + if (provider.equals(UpdateManager.TTS_PROVIDER_MISTRAL) && !UpdateManager.isMistralConfigured(this)) { + voiceSubtitleText.setText(english + ? "Mistral voices (requires API key - not configured!)" + : "Voces de Mistral (requiere clave API - ¡no configurada!)"); + } } private void updateInstalledVersionText() { diff --git a/app/src/main/java/com/bibliadiaria/app/UpdateManager.java b/app/src/main/java/com/bibliadiaria/app/UpdateManager.java index 7a651cf..e6145dc 100644 --- a/app/src/main/java/com/bibliadiaria/app/UpdateManager.java +++ b/app/src/main/java/com/bibliadiaria/app/UpdateManager.java @@ -29,12 +29,19 @@ final class UpdateManager { static final String KEY_TTS_VOICE_SPANISH = "tts_voice_spanish"; static final String DEFAULT_TTS_VOICE_ENGLISH = "af_heart"; static final String DEFAULT_TTS_VOICE_SPANISH = "ef_dora"; + static final String DEFAULT_TTS_VOICE_ENGLISH_MISTRAL = "v2/en_us-florence"; + static final String DEFAULT_TTS_VOICE_SPANISH_MISTRAL = "v2/es_es-carlota"; + static final String KEY_TTS_PROVIDER = "tts_provider"; + static final String KEY_MISTRAL_API_KEY = "mistral_api_key"; + static final String TTS_PROVIDER_GLEEZE = "gleeze"; + static final String TTS_PROVIDER_MISTRAL = "mistral"; + static final String MISTRAL_TTS_URL = "https://api.mistral.ai/v1/text-to-speech"; static final String LATEST_JSON_URL = "https://github.com/jahruz67/Bible-Daily/releases/download/latest/latest.json"; static final String APK_URL = "https://github.com/jahruz67/Bible-Daily/releases/download/latest/app-debug.apk"; - private static final VoiceOption[] ENGLISH_VOICES = { + private static final VoiceOption[] ENGLISH_VOICES_GLEEZE = { new VoiceOption("af_heart", "Heart (US female)"), new VoiceOption("af_alloy", "Alloy (US female)"), new VoiceOption("af_aoede", "Aoede (US female)"), @@ -65,12 +72,27 @@ final class UpdateManager { new VoiceOption("bm_lewis", "Lewis (UK male)") }; - private static final VoiceOption[] SPANISH_VOICES = { + private static final VoiceOption[] ENGLISH_VOICES_MISTRAL = { + new VoiceOption("v2/en_us-florence", "Florence (US female)"), + new VoiceOption("v2/en_us-dave", "Dave (US male)"), + new VoiceOption("v2/en_us-libby", "Libby (US female)"), + new VoiceOption("v2/en_us-matt", "Matt (US male)"), + new VoiceOption("v2/en_us-serena", "Serena (US female)"), + new VoiceOption("v2/en_us-andrew", "Andrew (US male)") + }; + + private static final VoiceOption[] SPANISH_VOICES_GLEEZE = { new VoiceOption("ef_dora", "Dora (Spanish female)"), new VoiceOption("em_alex", "Alex (Spanish male)"), new VoiceOption("em_santa", "Santa (Spanish male)") }; + private static final VoiceOption[] SPANISH_VOICES_MISTRAL = { + new VoiceOption("v2/es_es-alvaro", "Alvaro (Spanish male)"), + new VoiceOption("v2/es_es-carlota", "Carlota (Spanish female)"), + new VoiceOption("v2/es_es-gerardo", "Gerardo (Spanish male)") + }; + private UpdateManager() { } @@ -98,35 +120,76 @@ final class UpdateManager { preferences(context).edit().putBoolean(KEY_DARK_MODE, enabled).apply(); } - static VoiceOption[] getTtsVoiceOptions(boolean isEnglish) { - return isEnglish ? ENGLISH_VOICES : SPANISH_VOICES; + static VoiceOption[] getTtsVoiceOptions(boolean isEnglish, String provider) { + if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) { + return isEnglish ? ENGLISH_VOICES_GLEEZE : SPANISH_VOICES_GLEEZE; + } else { + return isEnglish ? ENGLISH_VOICES_MISTRAL : SPANISH_VOICES_MISTRAL; + } } - static String getTtsVoice(Context context, boolean isEnglish) { - String defaultVoice = getDefaultTtsVoice(isEnglish); - String voice = preferences(context).getString(getTtsVoiceKey(isEnglish), defaultVoice); - if (isValidTtsVoice(isEnglish, voice)) { + static VoiceOption[] getTtsVoiceOptions(boolean isEnglish) { + return getTtsVoiceOptions(isEnglish, TTS_PROVIDER_GLEEZE); + } + + static String getTtsVoice(Context context, boolean isEnglish, String provider) { + String defaultVoice = getDefaultTtsVoice(isEnglish, provider); + String voice = preferences(context).getString(getTtsVoiceKey(isEnglish, provider), defaultVoice); + if (isValidTtsVoice(isEnglish, provider, voice)) { return voice; } return defaultVoice; } - static void setTtsVoice(Context context, boolean isEnglish, String voiceId) { - String voice = isValidTtsVoice(isEnglish, voiceId) ? voiceId : getDefaultTtsVoice(isEnglish); - preferences(context).edit().putString(getTtsVoiceKey(isEnglish), voice).apply(); + static String getTtsVoice(Context context, boolean isEnglish) { + return getTtsVoice(context, isEnglish, TTS_PROVIDER_GLEEZE); } - static String getTtsVoiceLabel(boolean isEnglish, String voiceId) { - for (VoiceOption option : getTtsVoiceOptions(isEnglish)) { + static void setTtsVoice(Context context, boolean isEnglish, String provider, String voiceId) { + String voice = isValidTtsVoice(isEnglish, provider, voiceId) ? voiceId : getDefaultTtsVoice(isEnglish, provider); + preferences(context).edit().putString(getTtsVoiceKey(isEnglish, provider), voice).apply(); + } + + static void setTtsVoice(Context context, boolean isEnglish, String voiceId) { + setTtsVoice(context, isEnglish, TTS_PROVIDER_GLEEZE, voiceId); + } + + static String getTtsProvider(Context context) { + return preferences(context).getString(KEY_TTS_PROVIDER, TTS_PROVIDER_GLEEZE); + } + + static void setTtsProvider(Context context, String provider) { + preferences(context).edit().putString(KEY_TTS_PROVIDER, provider).apply(); + } + + static String getMistralApiKey(Context context) { + return preferences(context).getString(KEY_MISTRAL_API_KEY, ""); + } + + static void setMistralApiKey(Context context, String apiKey) { + preferences(context).edit().putString(KEY_MISTRAL_API_KEY, apiKey).apply(); + } + + static boolean isMistralConfigured(Context context) { + String apiKey = getMistralApiKey(context); + return apiKey != null && !apiKey.trim().isEmpty(); + } + + static String getTtsVoiceLabel(boolean isEnglish, String provider, String voiceId) { + for (VoiceOption option : getTtsVoiceOptions(isEnglish, provider)) { if (option.id.equals(voiceId)) { return option.label; } } - return getTtsVoiceLabel(isEnglish, getDefaultTtsVoice(isEnglish)); + return getTtsVoiceLabel(isEnglish, provider, getDefaultTtsVoice(isEnglish)); } - static int getTtsVoiceIndex(boolean isEnglish, String voiceId) { - VoiceOption[] options = getTtsVoiceOptions(isEnglish); + static String getTtsVoiceLabel(boolean isEnglish, String voiceId) { + return getTtsVoiceLabel(isEnglish, TTS_PROVIDER_GLEEZE, voiceId); + } + + static int getTtsVoiceIndex(boolean isEnglish, String provider, String voiceId) { + VoiceOption[] options = getTtsVoiceOptions(isEnglish, provider); for (int index = 0; index < options.length; index++) { if (options[index].id.equals(voiceId)) { return index; @@ -135,19 +198,39 @@ final class UpdateManager { return 0; } + static int getTtsVoiceIndex(boolean isEnglish, String voiceId) { + return getTtsVoiceIndex(isEnglish, TTS_PROVIDER_GLEEZE, voiceId); + } + + private static String getDefaultTtsVoice(boolean isEnglish, String provider) { + if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) { + return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH : DEFAULT_TTS_VOICE_SPANISH; + } else { + return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH_MISTRAL : DEFAULT_TTS_VOICE_SPANISH_MISTRAL; + } + } + private static String getDefaultTtsVoice(boolean isEnglish) { - return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH : DEFAULT_TTS_VOICE_SPANISH; + return getDefaultTtsVoice(isEnglish, TTS_PROVIDER_GLEEZE); + } + + private static String getTtsVoiceKey(boolean isEnglish, String provider) { + if (provider == null || provider.equals(TTS_PROVIDER_GLEEZE)) { + return isEnglish ? KEY_TTS_VOICE_ENGLISH : KEY_TTS_VOICE_SPANISH; + } else { + return isEnglish ? KEY_TTS_VOICE_ENGLISH + "_mistral" : KEY_TTS_VOICE_SPANISH + "_mistral"; + } } private static String getTtsVoiceKey(boolean isEnglish) { - return isEnglish ? KEY_TTS_VOICE_ENGLISH : KEY_TTS_VOICE_SPANISH; + return getTtsVoiceKey(isEnglish, TTS_PROVIDER_GLEEZE); } - private static boolean isValidTtsVoice(boolean isEnglish, String voiceId) { + private static boolean isValidTtsVoice(boolean isEnglish, String provider, String voiceId) { if (voiceId == null) { return false; } - for (VoiceOption option : getTtsVoiceOptions(isEnglish)) { + for (VoiceOption option : getTtsVoiceOptions(isEnglish, provider)) { if (option.id.equals(voiceId)) { return true; } @@ -155,6 +238,10 @@ final class UpdateManager { return false; } + private static boolean isValidTtsVoice(boolean isEnglish, String voiceId) { + return isValidTtsVoice(isEnglish, TTS_PROVIDER_GLEEZE, voiceId); + } + static UpdateInfo fetchLatestUpdate(Context context) throws Exception { String jsonText = download(LATEST_JSON_URL, "application/json"); JSONObject json = new JSONObject(jsonText);