mirror of
https://github.com/jahruz67/Bible-Daily.git
synced 2026-08-08 18:14:05 +00:00
Add Text-to-Speech functionality and voice selection in settings
- Implemented Text-to-Speech (TTS) feature in MainActivity for reading daily scriptures. - Added controls for TTS playback including play/pause, seek bar, and status updates. - Created a voice selection dialog in SettingsActivity to choose between available TTS voices. - Updated UpdateManager to manage TTS voice options and preferences. - Added a new drawable resource for speaker icon used in TTS controls.
This commit is contained in:
@@ -6,8 +6,11 @@ import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.media.MediaPlayer;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.Gravity;
|
||||
@@ -16,6 +19,7 @@ import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
@@ -23,9 +27,12 @@ import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -39,6 +46,8 @@ import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
private static final int COLOR_BACKGROUND = Color.rgb(247, 248, 246);
|
||||
private static final int COLOR_CARD = Color.WHITE;
|
||||
@@ -57,8 +66,21 @@ 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 final Locale spanishLocale = new Locale("es", "ES");
|
||||
private final List<TextView> resizableTextViews = new ArrayList<>();
|
||||
private final Handler ttsProgressHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable ttsProgressTick = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateTtsProgress();
|
||||
if (ttsPlayer != null && ttsPlayer.isPlaying()) {
|
||||
ttsProgressHandler.postDelayed(this, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private ExecutorService executor;
|
||||
private SharedPreferences preferences;
|
||||
@@ -78,9 +100,18 @@ public class MainActivity extends Activity {
|
||||
private Button retryButton;
|
||||
private LinearLayout fontPanel;
|
||||
private TextView fontValueText;
|
||||
private LinearLayout ttsPlayerPanel;
|
||||
private TextView ttsStatusText;
|
||||
private Button ttsPlayButton;
|
||||
private SeekBar ttsSeekBar;
|
||||
private TextView ttsTimeText;
|
||||
private MediaPlayer ttsPlayer;
|
||||
private File ttsAudioFile;
|
||||
private String currentTtsText = "";
|
||||
private Calendar selectedDateCalendar;
|
||||
private Calendar visibleMonthCalendar;
|
||||
private int loadGeneration;
|
||||
private int ttsGeneration;
|
||||
private float readingFontSp;
|
||||
private boolean isEnglish;
|
||||
|
||||
@@ -108,9 +139,14 @@ public class MainActivity extends Activity {
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
loadGeneration++;
|
||||
ttsGeneration++;
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
ttsProgressHandler.removeCallbacks(ttsProgressTick);
|
||||
releaseTtsPlayer();
|
||||
deleteTtsAudioFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1095,6 +1131,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private void showLoading(Date date, String url) {
|
||||
resetTtsForNewReading();
|
||||
resizableTextViews.clear();
|
||||
sectionsLayout.removeAllViews();
|
||||
statusBlock.setVisibility(View.VISIBLE);
|
||||
@@ -1113,6 +1150,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private void renderReading(DailyReading reading) {
|
||||
resetTtsForNewReading();
|
||||
statusBlock.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
retryButton.setVisibility(View.GONE);
|
||||
@@ -1145,6 +1183,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private void showError(Exception error, String url) {
|
||||
resetTtsForNewReading();
|
||||
statusBlock.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
retryButton.setVisibility(View.VISIBLE);
|
||||
@@ -1155,6 +1194,401 @@ public class MainActivity extends Activity {
|
||||
sourceText.setText((isEnglish ? "Intended source: " : "Fuente prevista: ") + "Vatican News\n" + url);
|
||||
}
|
||||
|
||||
private Button createOutlinedButton(String text) {
|
||||
Button button = new Button(this);
|
||||
button.setText(text);
|
||||
button.setTextColor(COLOR_ACCENT);
|
||||
button.setTextSize(14);
|
||||
button.setAllCaps(false);
|
||||
button.setBackground(roundedRect(Color.WHITE, dp(8), Color.rgb(219, 226, 222), dp(1)));
|
||||
return button;
|
||||
}
|
||||
|
||||
private void startTtsConversionForText(String text, TtsControls controls) {
|
||||
activateTtsControls(controls);
|
||||
currentTtsText = text == null ? "" : text;
|
||||
startTtsConversion();
|
||||
}
|
||||
|
||||
private void activateTtsControls(TtsControls controls) {
|
||||
if (controls == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ttsPlayerPanel != null && ttsPlayerPanel != controls.playerPanel) {
|
||||
ttsPlayerPanel.setVisibility(View.GONE);
|
||||
}
|
||||
if (ttsStatusText != null && ttsStatusText != controls.statusText) {
|
||||
ttsStatusText.setText("");
|
||||
ttsStatusText.setVisibility(View.GONE);
|
||||
}
|
||||
if (ttsPlayButton != null && ttsPlayButton != controls.playButton) {
|
||||
ttsPlayButton.setText(isEnglish ? "Play" : "Reproducir");
|
||||
}
|
||||
if (ttsSeekBar != null && ttsSeekBar != controls.seekBar) {
|
||||
ttsSeekBar.setProgress(0);
|
||||
ttsSeekBar.setMax(0);
|
||||
}
|
||||
if (ttsTimeText != null && ttsTimeText != controls.timeText) {
|
||||
ttsTimeText.setText("0:00");
|
||||
}
|
||||
|
||||
ttsStatusText = controls.statusText;
|
||||
ttsPlayerPanel = controls.playerPanel;
|
||||
ttsPlayButton = controls.playButton;
|
||||
ttsSeekBar = controls.seekBar;
|
||||
ttsTimeText = controls.timeText;
|
||||
}
|
||||
|
||||
private void startTtsConversion() {
|
||||
String text = currentTtsText == null ? "" : currentTtsText.trim();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int generation = ++ttsGeneration;
|
||||
stopTtsProgressUpdates();
|
||||
releaseTtsPlayer();
|
||||
deleteTtsAudioFile();
|
||||
setTtsStatus(isEnglish ? "Creating audio..." : "Generando audio...");
|
||||
if (ttsPlayerPanel != null) {
|
||||
ttsPlayerPanel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
File audioFile = downloadTtsAudio(text);
|
||||
runOnUiThread(() -> {
|
||||
if (generation == ttsGeneration) {
|
||||
showTtsAudioReady(audioFile);
|
||||
} else if (audioFile.exists()) {
|
||||
audioFile.delete();
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
if (generation == ttsGeneration) {
|
||||
showTtsError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private File downloadTtsAudio(String text) throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(TTS_SPEECH_URL).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");
|
||||
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("model", TTS_MODEL);
|
||||
body.put("voice", UpdateManager.getTtsVoice(this, isEnglish));
|
||||
body.put("input", text);
|
||||
body.put("response_format", "mp3");
|
||||
body.put("speed", 1.0);
|
||||
|
||||
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 ? "TTS API responded with code " : "La API TTS respondio con codigo ")
|
||||
+ 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();
|
||||
deleteTtsAudioFile();
|
||||
ttsAudioFile = audioFile;
|
||||
ttsPlayer = new MediaPlayer();
|
||||
ttsPlayer.setDataSource(audioFile.getAbsolutePath());
|
||||
ttsPlayer.setOnCompletionListener(player -> {
|
||||
stopTtsProgressUpdates();
|
||||
if (ttsPlayButton != null) {
|
||||
ttsPlayButton.setText(isEnglish ? "Play" : "Reproducir");
|
||||
}
|
||||
updateTtsProgress();
|
||||
});
|
||||
ttsPlayer.prepare();
|
||||
|
||||
if (ttsSeekBar != null) {
|
||||
ttsSeekBar.setMax(ttsPlayer.getDuration());
|
||||
ttsSeekBar.setProgress(0);
|
||||
}
|
||||
updateTtsTimeText(0, ttsPlayer.getDuration());
|
||||
if (ttsPlayerPanel != null) {
|
||||
ttsPlayerPanel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
setTtsStatus(isEnglish ? "Audio ready." : "Audio listo.");
|
||||
} catch (Exception error) {
|
||||
if (audioFile.exists()) {
|
||||
audioFile.delete();
|
||||
}
|
||||
showTtsError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private void showTtsError(Exception error) {
|
||||
releaseTtsPlayer();
|
||||
deleteTtsAudioFile();
|
||||
if (ttsPlayerPanel != null) {
|
||||
ttsPlayerPanel.setVisibility(View.GONE);
|
||||
}
|
||||
setTtsStatus((isEnglish ? "Could not create audio. " : "No se pudo crear el audio. ")
|
||||
+ cleanErrorMessage(error));
|
||||
}
|
||||
|
||||
private void toggleTtsPlayback() {
|
||||
if (ttsPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ttsPlayer.isPlaying()) {
|
||||
ttsPlayer.pause();
|
||||
stopTtsProgressUpdates();
|
||||
if (ttsPlayButton != null) {
|
||||
ttsPlayButton.setText(isEnglish ? "Play" : "Reproducir");
|
||||
}
|
||||
updateTtsProgress();
|
||||
} else {
|
||||
ttsPlayer.start();
|
||||
if (ttsPlayButton != null) {
|
||||
ttsPlayButton.setText(isEnglish ? "Pause" : "Pausar");
|
||||
}
|
||||
ttsProgressHandler.post(ttsProgressTick);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTtsProgress() {
|
||||
if (ttsPlayer == null || ttsSeekBar == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int position = ttsPlayer.getCurrentPosition();
|
||||
int duration = ttsPlayer.getDuration();
|
||||
ttsSeekBar.setProgress(position);
|
||||
updateTtsTimeText(position, duration);
|
||||
}
|
||||
|
||||
private void updateTtsTimeText(int positionMillis, int durationMillis) {
|
||||
if (ttsTimeText == null) {
|
||||
return;
|
||||
}
|
||||
ttsTimeText.setText(formatAudioTime(positionMillis) + " / " + formatAudioTime(durationMillis));
|
||||
}
|
||||
|
||||
private String formatAudioTime(int millis) {
|
||||
int totalSeconds = Math.max(0, millis / 1000);
|
||||
int minutes = totalSeconds / 60;
|
||||
int seconds = totalSeconds % 60;
|
||||
return String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||
}
|
||||
|
||||
private void setTtsStatus(String text) {
|
||||
if (ttsStatusText != null) {
|
||||
ttsStatusText.setText(text);
|
||||
ttsStatusText.setVisibility(text == null || text.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetTtsForNewReading() {
|
||||
ttsGeneration++;
|
||||
currentTtsText = "";
|
||||
stopTtsProgressUpdates();
|
||||
releaseTtsPlayer();
|
||||
deleteTtsAudioFile();
|
||||
if (ttsStatusText != null) {
|
||||
ttsStatusText.setText("");
|
||||
ttsStatusText.setVisibility(View.GONE);
|
||||
}
|
||||
if (ttsPlayerPanel != null) {
|
||||
ttsPlayerPanel.setVisibility(View.GONE);
|
||||
}
|
||||
if (ttsPlayButton != null) {
|
||||
ttsPlayButton.setText(isEnglish ? "Play" : "Reproducir");
|
||||
}
|
||||
if (ttsSeekBar != null) {
|
||||
ttsSeekBar.setProgress(0);
|
||||
ttsSeekBar.setMax(0);
|
||||
}
|
||||
if (ttsTimeText != null) {
|
||||
ttsTimeText.setText("0:00");
|
||||
}
|
||||
}
|
||||
|
||||
private void stopTtsProgressUpdates() {
|
||||
ttsProgressHandler.removeCallbacks(ttsProgressTick);
|
||||
}
|
||||
|
||||
private void releaseTtsPlayer() {
|
||||
if (ttsPlayer != null) {
|
||||
ttsPlayer.release();
|
||||
ttsPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteTtsAudioFile() {
|
||||
if (ttsAudioFile != null && ttsAudioFile.exists()) {
|
||||
ttsAudioFile.delete();
|
||||
}
|
||||
ttsAudioFile = null;
|
||||
}
|
||||
|
||||
private void addSpeechSection(List<String> parts, String title, DailySection section) {
|
||||
if (section == null || section.body.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(title);
|
||||
if (!section.introduction.isEmpty()) {
|
||||
builder.append("\n").append(section.introduction);
|
||||
}
|
||||
if (!section.reference.isEmpty()) {
|
||||
builder.append("\n").append(section.reference);
|
||||
}
|
||||
builder.append("\n").append(section.body);
|
||||
parts.add(builder.toString());
|
||||
}
|
||||
|
||||
private String buildSectionSpeechText(String title, DailySection section) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
addSpeechSection(parts, title, section);
|
||||
if (parts.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return normalizeWhitespace(parts.get(0));
|
||||
}
|
||||
|
||||
private View createSpeakerTitleRow(String title, int titleSizeSp, String speechText, TtsControls controls) {
|
||||
LinearLayout row = new LinearLayout(this);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||
|
||||
TextView titleText = textView(title, titleSizeSp, COLOR_INK, Typeface.BOLD);
|
||||
titleText.setIncludeFontPadding(false);
|
||||
row.addView(titleText, new LinearLayout.LayoutParams(
|
||||
0,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
));
|
||||
|
||||
ImageButton speakerButton = new ImageButton(this);
|
||||
speakerButton.setImageResource(R.drawable.ic_speaker_24);
|
||||
speakerButton.setColorFilter(COLOR_ACCENT);
|
||||
speakerButton.setPadding(dp(8), dp(8), dp(8), dp(8));
|
||||
speakerButton.setBackground(roundedRect(Color.rgb(229, 244, 240), dp(8), Color.TRANSPARENT, 0));
|
||||
speakerButton.setContentDescription((isEnglish ? "Listen to " : "Escuchar ") + title);
|
||||
speakerButton.setOnClickListener(view -> startTtsConversionForText(speechText, controls));
|
||||
|
||||
LinearLayout.LayoutParams speakerParams = new LinearLayout.LayoutParams(dp(40), dp(40));
|
||||
speakerParams.setMargins(dp(8), 0, 0, 0);
|
||||
row.addView(speakerButton, speakerParams);
|
||||
return row;
|
||||
}
|
||||
|
||||
private TtsControls createTtsControls() {
|
||||
TextView status = textView("", 14, COLOR_MUTED, Typeface.BOLD);
|
||||
status.setVisibility(View.GONE);
|
||||
|
||||
LinearLayout playerPanel = new LinearLayout(this);
|
||||
playerPanel.setOrientation(LinearLayout.HORIZONTAL);
|
||||
playerPanel.setGravity(Gravity.CENTER_VERTICAL);
|
||||
playerPanel.setVisibility(View.GONE);
|
||||
|
||||
Button playButton = createOutlinedButton(isEnglish ? "Play" : "Reproducir");
|
||||
playerPanel.addView(playButton, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
dp(42)
|
||||
));
|
||||
|
||||
SeekBar seekBar = new SeekBar(this);
|
||||
seekBar.setMax(0);
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser && seekBar == ttsSeekBar && ttsPlayer != null) {
|
||||
ttsPlayer.seekTo(progress);
|
||||
updateTtsTimeText(progress, ttsPlayer.getDuration());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
LinearLayout.LayoutParams seekParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
seekParams.setMargins(dp(8), 0, dp(8), 0);
|
||||
playerPanel.addView(seekBar, seekParams);
|
||||
|
||||
TextView timeText = textView("0:00", 13, COLOR_MUTED, Typeface.BOLD);
|
||||
timeText.setGravity(Gravity.CENTER_VERTICAL);
|
||||
playerPanel.addView(timeText, new LinearLayout.LayoutParams(
|
||||
dp(86),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
TtsControls controls = new TtsControls(status, playerPanel, playButton, seekBar, timeText);
|
||||
playButton.setOnClickListener(view -> {
|
||||
activateTtsControls(controls);
|
||||
toggleTtsPlayback();
|
||||
});
|
||||
return controls;
|
||||
}
|
||||
|
||||
private void addTtsControlsToCard(LinearLayout card, TtsControls controls) {
|
||||
LinearLayout.LayoutParams statusParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
statusParams.setMargins(0, 0, 0, dp(10));
|
||||
card.addView(controls.statusText, statusParams);
|
||||
|
||||
LinearLayout.LayoutParams playerParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
playerParams.setMargins(0, 0, 0, dp(14));
|
||||
card.addView(controls.playerPanel, playerParams);
|
||||
}
|
||||
|
||||
private void addScriptureCard(String title, String overline, DailySection section, boolean featured) {
|
||||
if (section == null || section.body.isEmpty()) {
|
||||
return;
|
||||
@@ -1167,14 +1601,20 @@ public class MainActivity extends Activity {
|
||||
card.addView(overlineText);
|
||||
}
|
||||
|
||||
TextView titleText = textView(title, featured ? 28 : 22, COLOR_INK, Typeface.BOLD);
|
||||
titleText.setIncludeFontPadding(false);
|
||||
TtsControls ttsControls = createTtsControls();
|
||||
View titleRow = createSpeakerTitleRow(
|
||||
title,
|
||||
featured ? 28 : 22,
|
||||
buildSectionSpeechText(title, section),
|
||||
ttsControls
|
||||
);
|
||||
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
titleParams.setMargins(0, overline == null ? 0 : dp(8), 0, dp(12));
|
||||
card.addView(titleText, titleParams);
|
||||
card.addView(titleRow, titleParams);
|
||||
addTtsControlsToCard(card, ttsControls);
|
||||
|
||||
if (!section.introduction.isEmpty()) {
|
||||
TextView introText = textView(section.introduction, 16, COLOR_MUTED, Typeface.BOLD);
|
||||
@@ -1211,9 +1651,15 @@ public class MainActivity extends Activity {
|
||||
|
||||
LinearLayout card = createCard(dp(18));
|
||||
|
||||
TextView title = textView(isEnglish ? "The Pope's words" : "Las palabras de los Papas", 22, COLOR_INK, Typeface.BOLD);
|
||||
title.setIncludeFontPadding(false);
|
||||
card.addView(title);
|
||||
String title = isEnglish ? "The Pope's words" : "Las palabras de los Papas";
|
||||
TtsControls ttsControls = createTtsControls();
|
||||
card.addView(createSpeakerTitleRow(
|
||||
title,
|
||||
22,
|
||||
buildSectionSpeechText(title, section),
|
||||
ttsControls
|
||||
));
|
||||
addTtsControlsToCard(card, ttsControls);
|
||||
|
||||
TextView body = textView(section.body, Math.round(readingFontSp), COLOR_INK, Typeface.NORMAL);
|
||||
body.setLineSpacing(dp(4), 1.16f);
|
||||
@@ -1670,6 +2116,28 @@ public class MainActivity extends Activity {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private static class TtsControls {
|
||||
final TextView statusText;
|
||||
final LinearLayout playerPanel;
|
||||
final Button playButton;
|
||||
final SeekBar seekBar;
|
||||
final TextView timeText;
|
||||
|
||||
TtsControls(
|
||||
TextView statusText,
|
||||
LinearLayout playerPanel,
|
||||
Button playButton,
|
||||
SeekBar seekBar,
|
||||
TextView timeText
|
||||
) {
|
||||
this.statusText = statusText;
|
||||
this.playerPanel = playerPanel;
|
||||
this.playButton = playButton;
|
||||
this.seekBar = seekBar;
|
||||
this.timeText = timeText;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LiturgicalDay {
|
||||
final String englishName;
|
||||
final String spanishName;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bibliadiaria.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
@@ -31,6 +32,8 @@ public class SettingsActivity extends Activity {
|
||||
private TextView installedVersionText;
|
||||
private TextView checkButton;
|
||||
private TextView downloadButton;
|
||||
private TextView voiceSubtitleText;
|
||||
private TextView voiceButton;
|
||||
private UpdateManager.UpdateInfo latestInfo;
|
||||
|
||||
@Override
|
||||
@@ -83,6 +86,7 @@ public class SettingsActivity extends Activity {
|
||||
content.addView(title, titleParams);
|
||||
|
||||
content.addView(createLanguageCard());
|
||||
content.addView(createVoiceCard());
|
||||
content.addView(createVersionCard());
|
||||
|
||||
checkButton = actionButton("Check now", COLOR_ACCENT, Color.WHITE);
|
||||
@@ -182,12 +186,50 @@ public class SettingsActivity extends Activity {
|
||||
boolean current = UpdateManager.isEnglish(this);
|
||||
UpdateManager.setEnglish(this, !current);
|
||||
langToggle.setText(!current ? "English" : "Spanish");
|
||||
updateVoiceCard();
|
||||
});
|
||||
card.addView(langToggle, new LinearLayout.LayoutParams(dp(100), dp(40)));
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
private View createVoiceCard() {
|
||||
LinearLayout card = createCard();
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
LinearLayout copy = new LinearLayout(this);
|
||||
copy.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
TextView title = textView("Voice", 20, COLOR_INK, Typeface.BOLD);
|
||||
title.setIncludeFontPadding(false);
|
||||
copy.addView(title);
|
||||
|
||||
voiceSubtitleText = textView("", 14, COLOR_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(voiceSubtitleText, subtitleParams);
|
||||
|
||||
card.addView(copy, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
voiceButton = actionButton("", COLOR_ACCENT, Color.WHITE);
|
||||
voiceButton.setOnClickListener(view -> showVoicePicker());
|
||||
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
dp(42)
|
||||
);
|
||||
buttonParams.setMargins(0, dp(12), 0, 0);
|
||||
card.addView(voiceButton, buttonParams);
|
||||
|
||||
updateVoiceCard();
|
||||
return card;
|
||||
}
|
||||
|
||||
private View createVersionCard() {
|
||||
LinearLayout card = createCard();
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
@@ -261,6 +303,43 @@ public class SettingsActivity extends Activity {
|
||||
statusText.setText("Could not check for updates. " + cleanMessage(error));
|
||||
}
|
||||
|
||||
private void showVoicePicker() {
|
||||
boolean english = UpdateManager.isEnglish(this);
|
||||
UpdateManager.VoiceOption[] options = UpdateManager.getTtsVoiceOptions(english);
|
||||
String[] labels = new String[options.length];
|
||||
for (int index = 0; index < options.length; index++) {
|
||||
labels[index] = options[index].label;
|
||||
}
|
||||
|
||||
int selectedIndex = UpdateManager.getTtsVoiceIndex(
|
||||
english,
|
||||
UpdateManager.getTtsVoice(this, english)
|
||||
);
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(english ? "English voice" : "Spanish voice")
|
||||
.setSingleChoiceItems(labels, selectedIndex, (dialog, which) -> {
|
||||
UpdateManager.setTtsVoice(this, english, options[which].id);
|
||||
updateVoiceCard();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateVoiceCard() {
|
||||
if (voiceSubtitleText == null || voiceButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean english = UpdateManager.isEnglish(this);
|
||||
String voiceId = UpdateManager.getTtsVoice(this, english);
|
||||
voiceSubtitleText.setText(english
|
||||
? "English voices are shown while English is selected"
|
||||
: "Spanish voices are shown while Spanish is selected");
|
||||
voiceButton.setText(UpdateManager.getTtsVoiceLabel(english, voiceId));
|
||||
}
|
||||
|
||||
private void updateInstalledVersionText() {
|
||||
try {
|
||||
installedVersionText.setText("Installed: "
|
||||
|
||||
@@ -24,11 +24,52 @@ final class UpdateManager {
|
||||
static final String PREFS_NAME = "preferencias_lectura";
|
||||
static final String KEY_AUTO_UPDATES = "auto_updates_enabled";
|
||||
static final String KEY_IS_ENGLISH = "is_english";
|
||||
static final String KEY_TTS_VOICE_ENGLISH = "tts_voice_english";
|
||||
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 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 = {
|
||||
new VoiceOption("af_heart", "Heart (US female)"),
|
||||
new VoiceOption("af_alloy", "Alloy (US female)"),
|
||||
new VoiceOption("af_aoede", "Aoede (US female)"),
|
||||
new VoiceOption("af_bella", "Bella (US female)"),
|
||||
new VoiceOption("af_jadzia", "Jadzia (US female)"),
|
||||
new VoiceOption("af_jessica", "Jessica (US female)"),
|
||||
new VoiceOption("af_kore", "Kore (US female)"),
|
||||
new VoiceOption("af_nicole", "Nicole (US female)"),
|
||||
new VoiceOption("af_nova", "Nova (US female)"),
|
||||
new VoiceOption("af_river", "River (US female)"),
|
||||
new VoiceOption("af_sarah", "Sarah (US female)"),
|
||||
new VoiceOption("af_sky", "Sky (US female)"),
|
||||
new VoiceOption("am_adam", "Adam (US male)"),
|
||||
new VoiceOption("am_echo", "Echo (US male)"),
|
||||
new VoiceOption("am_eric", "Eric (US male)"),
|
||||
new VoiceOption("am_fenrir", "Fenrir (US male)"),
|
||||
new VoiceOption("am_liam", "Liam (US male)"),
|
||||
new VoiceOption("am_michael", "Michael (US male)"),
|
||||
new VoiceOption("am_onyx", "Onyx (US male)"),
|
||||
new VoiceOption("am_puck", "Puck (US male)"),
|
||||
new VoiceOption("am_santa", "Santa (US male)"),
|
||||
new VoiceOption("bf_alice", "Alice (UK female)"),
|
||||
new VoiceOption("bf_emma", "Emma (UK female)"),
|
||||
new VoiceOption("bf_lily", "Lily (UK female)"),
|
||||
new VoiceOption("bm_daniel", "Daniel (UK male)"),
|
||||
new VoiceOption("bm_fable", "Fable (UK male)"),
|
||||
new VoiceOption("bm_george", "George (UK male)"),
|
||||
new VoiceOption("bm_lewis", "Lewis (UK male)")
|
||||
};
|
||||
|
||||
private static final VoiceOption[] SPANISH_VOICES = {
|
||||
new VoiceOption("ef_dora", "Dora (Spanish female)"),
|
||||
new VoiceOption("em_alex", "Alex (Spanish male)"),
|
||||
new VoiceOption("em_santa", "Santa (Spanish male)")
|
||||
};
|
||||
|
||||
private UpdateManager() {
|
||||
}
|
||||
|
||||
@@ -48,6 +89,63 @@ final class UpdateManager {
|
||||
preferences(context).edit().putBoolean(KEY_IS_ENGLISH, enabled).apply();
|
||||
}
|
||||
|
||||
static VoiceOption[] getTtsVoiceOptions(boolean isEnglish) {
|
||||
return isEnglish ? ENGLISH_VOICES : SPANISH_VOICES;
|
||||
}
|
||||
|
||||
static String getTtsVoice(Context context, boolean isEnglish) {
|
||||
String defaultVoice = getDefaultTtsVoice(isEnglish);
|
||||
String voice = preferences(context).getString(getTtsVoiceKey(isEnglish), defaultVoice);
|
||||
if (isValidTtsVoice(isEnglish, 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 getTtsVoiceLabel(boolean isEnglish, String voiceId) {
|
||||
for (VoiceOption option : getTtsVoiceOptions(isEnglish)) {
|
||||
if (option.id.equals(voiceId)) {
|
||||
return option.label;
|
||||
}
|
||||
}
|
||||
return getTtsVoiceLabel(isEnglish, getDefaultTtsVoice(isEnglish));
|
||||
}
|
||||
|
||||
static int getTtsVoiceIndex(boolean isEnglish, String voiceId) {
|
||||
VoiceOption[] options = getTtsVoiceOptions(isEnglish);
|
||||
for (int index = 0; index < options.length; index++) {
|
||||
if (options[index].id.equals(voiceId)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static String getDefaultTtsVoice(boolean isEnglish) {
|
||||
return isEnglish ? DEFAULT_TTS_VOICE_ENGLISH : DEFAULT_TTS_VOICE_SPANISH;
|
||||
}
|
||||
|
||||
private static String getTtsVoiceKey(boolean isEnglish) {
|
||||
return isEnglish ? KEY_TTS_VOICE_ENGLISH : KEY_TTS_VOICE_SPANISH;
|
||||
}
|
||||
|
||||
private static boolean isValidTtsVoice(boolean isEnglish, String voiceId) {
|
||||
if (voiceId == null) {
|
||||
return false;
|
||||
}
|
||||
for (VoiceOption option : getTtsVoiceOptions(isEnglish)) {
|
||||
if (option.id.equals(voiceId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static UpdateInfo fetchLatestUpdate(Context context) throws Exception {
|
||||
String jsonText = download(LATEST_JSON_URL, "application/json");
|
||||
JSONObject json = new JSONObject(jsonText);
|
||||
@@ -187,4 +285,14 @@ final class UpdateManager {
|
||||
this.updateAvailable = updateAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
static final class VoiceOption {
|
||||
final String id;
|
||||
final String label;
|
||||
|
||||
VoiceOption(String id, String label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1,-3.29 -2.5,-4.03v8.05c1.5,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z" />
|
||||
</vector>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user