package net.bible.android.control.speak;
import android.media.AudioManager;
import android.util.Log;
import android.widget.Toast;
import net.bible.android.BibleApplication;
import net.bible.android.activity.R;
import net.bible.android.control.ApplicationScope;
import net.bible.android.control.page.CurrentPage;
import net.bible.android.control.page.window.ActiveWindowPageManagerProvider;
import net.bible.android.view.activity.base.CurrentActivityHolder;
import net.bible.service.common.AndRuntimeException;
import net.bible.service.common.CommonUtils;
import net.bible.service.device.speak.TextToSpeechServiceManager;
import net.bible.service.sword.SwordContentFacade;
import org.crosswire.jsword.book.Book;
import org.crosswire.jsword.book.BookCategory;
import org.crosswire.jsword.book.sword.SwordBook;
import org.crosswire.jsword.passage.Key;
import org.crosswire.jsword.passage.KeyUtil;
import org.crosswire.jsword.passage.Verse;
import org.crosswire.jsword.versification.Versification;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import dagger.Lazy;
/**
* @author Martin Denham [mjdenham at gmail dot com]
* @see gnu.lgpl.License for license details.<br>
* The copyright to this program is held by it's author.
*/
@ApplicationScope
public class SpeakControl {
private Lazy<TextToSpeechServiceManager> textToSpeechServiceManager;
private final SwordContentFacade swordContentFacade;
private final ActiveWindowPageManagerProvider activeWindowPageManagerProvider;
private static final int NUM_LEFT_IDX = 3;
private static final NumPagesToSpeakDefinition[] BIBLE_PAGES_TO_SPEAK_DEFNS = new NumPagesToSpeakDefinition[] {
new NumPagesToSpeakDefinition(1, R.plurals.num_chapters, true, R.id.numChapters1),
new NumPagesToSpeakDefinition(2, R.plurals.num_chapters, true, R.id.numChapters2),
new NumPagesToSpeakDefinition(5, R.plurals.num_chapters, true, R.id.numChapters3),
new NumPagesToSpeakDefinition(10, R.string.rest_of_book, false, R.id.numChapters4)
};
private static final NumPagesToSpeakDefinition[] COMMENTARY_PAGES_TO_SPEAK_DEFNS = new NumPagesToSpeakDefinition[] {
new NumPagesToSpeakDefinition(1, R.plurals.num_verses, true, R.id.numChapters1),
new NumPagesToSpeakDefinition(2, R.plurals.num_verses, true, R.id.numChapters2),
new NumPagesToSpeakDefinition(5, R.plurals.num_verses, true, R.id.numChapters3),
new NumPagesToSpeakDefinition(10, R.string.rest_of_chapter, false, R.id.numChapters4)
};
private static final NumPagesToSpeakDefinition[] DEFAULT_PAGES_TO_SPEAK_DEFNS = new NumPagesToSpeakDefinition[] {
new NumPagesToSpeakDefinition(1, R.plurals.num_pages, true, R.id.numChapters1),
new NumPagesToSpeakDefinition(2, R.plurals.num_pages, true, R.id.numChapters2),
new NumPagesToSpeakDefinition(5, R.plurals.num_pages, true, R.id.numChapters3),
new NumPagesToSpeakDefinition(10, R.plurals.num_pages, true, R.id.numChapters4)
};
private static final String TAG = "SpeakControl";
@Inject
public SpeakControl(Lazy<TextToSpeechServiceManager> textToSpeechServiceManager, SwordContentFacade swordContentFacade, ActiveWindowPageManagerProvider activeWindowPageManagerProvider) {
this.textToSpeechServiceManager = textToSpeechServiceManager;
this.swordContentFacade = swordContentFacade;
this.activeWindowPageManagerProvider = activeWindowPageManagerProvider;
}
/** return a list of prompt ids for the speak screen associated with the current document type
*/
public NumPagesToSpeakDefinition[] getNumPagesToSpeakDefinitions() {
NumPagesToSpeakDefinition[] definitions;
CurrentPage currentPage = activeWindowPageManagerProvider.getActiveWindowPageManager().getCurrentPage();
BookCategory bookCategory = currentPage.getCurrentDocument().getBookCategory();
if (BookCategory.BIBLE.equals(bookCategory)) {
Versification v11n = ((SwordBook) currentPage.getCurrentDocument()).getVersification();
Verse verse = KeyUtil.getVerse(currentPage.getSingleKey());
int chaptersLeft = 0;
try {
chaptersLeft = v11n.getLastChapter(verse.getBook()) - verse.getChapter() + 1;
} catch (Exception e) {
Log.e(TAG, "Error in book no", e);
}
definitions = BIBLE_PAGES_TO_SPEAK_DEFNS;
definitions[NUM_LEFT_IDX].setNumPages(chaptersLeft);
} else if (BookCategory.COMMENTARY.equals(bookCategory)) {
Versification v11n = ((SwordBook) currentPage.getCurrentDocument()).getVersification();
Verse verse = KeyUtil.getVerse(currentPage.getSingleKey());
int versesLeft = 0;
try {
versesLeft = v11n.getLastVerse(verse.getBook(), verse.getChapter()) - verse.getVerse() + 1;
} catch (Exception e) {
Log.e(TAG, "Error in book no", e);
}
definitions = COMMENTARY_PAGES_TO_SPEAK_DEFNS;
definitions[NUM_LEFT_IDX].setNumPages(versesLeft);
} else {
definitions = DEFAULT_PAGES_TO_SPEAK_DEFNS;
}
return definitions;
}
/** Toggle speech - prepare to speak single page OR if speaking then stop speaking
*/
public void speakToggleCurrentPage() {
Log.d(TAG, "Speak toggle current page");
// Continue
if (isPaused()) {
continueAfterPause();
//Pause
} else if (isSpeaking()) {
pause();
// Start Speak
} else {
try {
CurrentPage page = activeWindowPageManagerProvider.getActiveWindowPageManager().getCurrentPage();
Book fromBook = page.getCurrentDocument();
// first find keys to Speak
List<Key> keyList = new ArrayList<>();
keyList.add(page.getKey());
speak(fromBook, keyList, true, false);
Toast.makeText(BibleApplication.getApplication(), R.string.speak, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "Error getting chapters to speak", e);
throw new AndRuntimeException("Error preparing Speech", e);
}
}
}
public boolean isCurrentDocSpeakAvailable() {
boolean isAvailable;
try {
String docLangCode = activeWindowPageManagerProvider.getActiveWindowPageManager().getCurrentPage().getCurrentDocument().getLanguage().getCode();
isAvailable = textToSpeechServiceManager.get().isLanguageAvailable(docLangCode);
} catch (Exception e) {
Log.e(TAG, "Error checking TTS lang available");
isAvailable = false;
}
return isAvailable;
}
public boolean isSpeaking() {
return textToSpeechServiceManager.get().isSpeaking();
}
public boolean isPaused() {
return textToSpeechServiceManager.get().isPaused();
}
/** prepare to speak
*/
public void speak(NumPagesToSpeakDefinition numPagesDefn, boolean queue, boolean repeat) {
Log.d(TAG, "Chapters:"+numPagesDefn.getNumPages());
// if a previous speak request is paused clear the cached text
if (isPaused()) {
Log.d(TAG, "Clearing paused Speak text");
stop();
}
CurrentPage page = activeWindowPageManagerProvider.getActiveWindowPageManager().getCurrentPage();
Book fromBook = page.getCurrentDocument()
;
// first find keys to Speak
List<Key> keyList = new ArrayList<>();
try {
for (int i=0; i<numPagesDefn.getNumPages(); i++) {
Key key = page.getPagePlus(i);
if (key!=null) {
keyList.add(key);
}
}
speak(fromBook, keyList, queue, repeat);
} catch (Exception e) {
Log.e(TAG, "Error getting chapters to speak", e);
throw new AndRuntimeException("Error preparing Speech", e);
}
}
public void speak(Book book, List<Key> keyList, boolean queue, boolean repeat) {
Log.d(TAG, "Keys:"+keyList.size());
// build a string containing the text to be spoken
List<String> textToSpeak = new ArrayList<>();
// first concatenate the number of required chapters
try {
for (Key key : keyList) {
// intro
textToSpeak.add(key.getName()+". ");
// textToSpeak.add("\n");
// content
textToSpeak.add( swordContentFacade.getTextToSpeak(book, key));
// add a pause at end to separate passages
textToSpeak.add("\n");
}
} catch (Exception e) {
Log.e(TAG, "Error getting chapters to speak", e);
throw new AndRuntimeException("Error preparing Speech", e);
}
// if repeat was checked then concatenate with itself
if (repeat) {
textToSpeak.add("\n");
textToSpeak.addAll(textToSpeak);
}
speak(textToSpeak, book, queue);
}
/** prepare to speak
*/
private void speak(List<String> textsToSpeak, Book fromBook, boolean queue) {
List<Locale> localePreferenceList = calculateLocalePreferenceList(fromBook);
preSpeak();
// speak current chapter or stop speech if already speaking
Log.d(TAG, "Tell TTS to speak");
textToSpeechServiceManager.get().speak(localePreferenceList, textsToSpeak, queue);
}
public void rewind() {
if (isSpeaking() || isPaused()) {
Log.d(TAG, "Rewind TTS speaking");
textToSpeechServiceManager.get().rewind();
Toast.makeText(BibleApplication.getApplication(), R.string.rewind, Toast.LENGTH_SHORT).show();
}
}
public void forward() {
if (isSpeaking() || isPaused()) {
Log.d(TAG, "Forward TTS speaking");
textToSpeechServiceManager.get().forward();
Toast.makeText(BibleApplication.getApplication(), R.string.forward, Toast.LENGTH_SHORT).show();
}
}
public void pause() {
if (isSpeaking() || isPaused()) {
Log.d(TAG, "Pause TTS speaking");
TextToSpeechServiceManager tts = textToSpeechServiceManager.get();
tts.pause();
String pause = CommonUtils.getResourceString(R.string.pause);
String timeProgress = CommonUtils.getHoursMinsSecs(tts.getPausedCompletedSeconds())+"/"+CommonUtils.getHoursMinsSecs(tts.getPausedTotalSeconds());
Toast.makeText(BibleApplication.getApplication(), pause+"\n"+timeProgress, Toast.LENGTH_SHORT).show();
}
}
public void continueAfterPause() {
Log.d(TAG, "Continue TTS speaking after pause");
preSpeak();
textToSpeechServiceManager.get().continueAfterPause();
Toast.makeText(BibleApplication.getApplication(), R.string.speak, Toast.LENGTH_SHORT).show();
}
public void stop() {
Log.d(TAG, "Stop TTS speaking");
doStop();
Toast.makeText(BibleApplication.getApplication(), R.string.stop, Toast.LENGTH_SHORT).show();
}
private void doStop() {
textToSpeechServiceManager.get().shutdown();
}
private void preSpeak() {
// ensure volume controls adjust correct stream - not phone which is the default
// STREAM_TTS does not seem to be available but this article says use STREAM_MUSIC instead: http://stackoverflow.com/questions/7558650/how-to-set-volume-for-text-to-speech-speak-method
CurrentActivityHolder.getInstance().getCurrentActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
private List<Locale> calculateLocalePreferenceList(Book fromBook) {
//calculate preferred locales to use for speech
// Set preferred language to the same language as the book.
// Note that a language may not be available, and so we have a preference list
String bookLanguageCode = fromBook.getLanguage().getCode();
Log.d(TAG, "Book has language code:"+bookLanguageCode);
List<Locale> localePreferenceList = new ArrayList<>();
if (bookLanguageCode.equals(Locale.getDefault().getLanguage())) {
// for people in UK the UK accent is preferable to the US accent
localePreferenceList.add( Locale.getDefault() );
}
// try to get the native country for the lang
String countryCode = getDefaultCountryCode(bookLanguageCode);
if (countryCode!=null) {
localePreferenceList.add( new Locale(bookLanguageCode, countryCode));
}
// finally just add the language of the book
localePreferenceList.add( new Locale(bookLanguageCode));
return localePreferenceList;
}
private String getDefaultCountryCode(String language) {
if (language.equals("en")) return Locale.UK.getCountry();
if (language.equals("fr")) return Locale.FRANCE.getCountry();
if (language.equals("de")) return Locale.GERMANY.getCountry();
if (language.equals("zh")) return Locale.CHINA.getCountry();
if (language.equals("it")) return Locale.ITALY.getCountry();
if (language.equals("jp")) return Locale.JAPAN.getCountry();
if (language.equals("ko")) return Locale.KOREA.getCountry();
if (language.equals("hu")) return "HU";
if (language.equals("cs")) return "CZ";
if (language.equals("fi")) return "FI";
if (language.equals("pl")) return "PL";
if (language.equals("pt")) return "PT";
if (language.equals("ru")) return "RU";
if (language.equals("tr")) return "TR";
return null;
}
}