package net.osmand.plus.voice; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech.OnInitListener; import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener; import android.support.v7.app.AlertDialog; import net.osmand.PlatformUtil; import net.osmand.plus.ApplicationMode; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.activities.SettingsActivity; import net.osmand.plus.routing.VoiceRouter; import net.osmand.util.Algorithms; import org.apache.commons.logging.Log; import org.json.JSONArray; import org.json.JSONObject; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public class TTSCommandPlayerImpl extends AbstractPrologCommandPlayer { public final static String PEBBLE_ALERT = "PEBBLE_ALERT"; public final static String WEAR_ALERT = "WEAR_ALERT"; private static final class IntentStarter implements DialogInterface.OnClickListener { private final Context ctx; private final String intentAction; private final Uri intentData; private IntentStarter(Context ctx, String intentAction) { this(ctx,intentAction, null); } private IntentStarter(Context ctx, String intentAction, Uri intentData) { this.ctx = ctx; this.intentAction = intentAction; this.intentData = intentData; } @Override public void onClick(DialogInterface dialog, int which) { Intent installIntent = new Intent(); installIntent.setAction(intentAction); if (intentData != null) { installIntent.setData(intentData); } ctx.startActivity(installIntent); } } private static final String CONFIG_FILE = "_ttsconfig.p"; private static final int[] TTS_VOICE_VERSION = new int[] { 102, 103 }; // !! MUST BE SORTED // No more TTS v101 support because of too many changes // TODO: We could actually remove v102 support, I am done updating all existing 35 TTS voices to v103. Hardy, July 2016 private static final Log log = PlatformUtil.getLog(TTSCommandPlayerImpl.class); private static TextToSpeech mTts; private static String ttsVoiceName = ""; private Context mTtsContext; private HashMap<String, String> params = new HashMap<String, String>(); private VoiceRouter vrt; public TTSCommandPlayerImpl(Activity ctx, ApplicationMode applicationMode, VoiceRouter vrt, String voiceProvider) throws CommandPlayerException { super((OsmandApplication) ctx.getApplicationContext(), applicationMode, voiceProvider, CONFIG_FILE, TTS_VOICE_VERSION); this.vrt = vrt; if (Algorithms.isEmpty(language)) { throw new CommandPlayerException( ctx.getString(R.string.voice_data_corrupted)); } OsmandApplication app = (OsmandApplication) ctx.getApplicationContext(); if(app.accessibilityEnabled()) { cSpeechRate = app.getSettings().SPEECH_RATE.get(); } initializeEngine(app, ctx); params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, app.getSettings().AUDIO_STREAM_GUIDANCE .getModeValue(getApplicationMode()).toString()); } /** * Since TTS requests are asynchronous, playCommands() can be called before * the TTS engine is done. We use this field to keep track of concurrent tts * activity. Where tts activity is defined as the time between tts.speak() * and the call back to onUtteranceCompletedListener(). This allows us to * optimize use of requesting and abandoning audio focus. */ private static int ttsRequests; private float cSpeechRate = 1; private boolean speechAllowed = false; // Called from the calculating route thread. @Override public synchronized void playCommands(CommandBuilder builder) { final List<String> execute = builder.execute(); //list of strings, the speech text, play it StringBuilder bld = new StringBuilder(); for (String s : execute) { bld.append(s).append(' '); } sendAlertToPebble(bld.toString()); if (mTts != null && !vrt.isMute() && speechAllowed) { if (ttsRequests++ == 0) { requestAudioFocus(); // Delay first prompt of each batch to allow BT SCO connection being established if (ctx.getSettings().AUDIO_STREAM_GUIDANCE.getModeValue(getApplicationMode()) == 0) { ttsRequests++; if (android.os.Build.VERSION.SDK_INT < 21) { params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,""+System.currentTimeMillis()); mTts.playSilence(ctx.getSettings().BT_SCO_DELAY.get(), TextToSpeech.QUEUE_ADD, params); } else { mTts.playSilentUtterance(ctx.getSettings().BT_SCO_DELAY.get(), TextToSpeech.QUEUE_ADD, ""+System.currentTimeMillis()); } } } log.debug("ttsRequests="+ttsRequests); params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,""+System.currentTimeMillis()); mTts.speak(bld.toString(), TextToSpeech.QUEUE_ADD, params); // Audio focus will be released when onUtteranceCompleted() completed is called by the TTS engine. } else if (ctx != null && vrt.isMute()) { // sendAlertToAndroidWear(ctx, bld.toString()); } } @Override public void stop(){ ttsRequests = 0; if (mTts != null){ mTts.stop(); } abandonAudioFocus(); } public void sendAlertToPebble(String bld) { final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION"); final Map<String, Object> data = new HashMap<String, Object>(); data.put("title", "Voice"); data.put("body", bld.toString()); final JSONObject jsonData = new JSONObject(data); final String notificationData = new JSONArray().put(jsonData).toString(); i.putExtra("messageType", PEBBLE_ALERT); i.putExtra("sender", "OsmAnd"); i.putExtra("notificationData", notificationData); if (ctx != null) { ctx.sendBroadcast(i); log.info("Send message to pebble " + bld.toString()); } } private void initializeEngine(final Context ctx, final Activity act) { if (mTtsContext != ctx) { internalClear(); } if (mTts == null) { mTtsContext = ctx; ttsVoiceName = ""; ttsRequests = 0; final float speechRate = cSpeechRate; final String[] lsplit = (language + "____.").split("[\\_\\-]"); // constructor supports lang_country_variant Locale newLocale0 = new Locale(lsplit[0], lsplit[1], lsplit[2]); // #3344: Try Locale builder instead of constructor (only available from API 21). Also supports script (for now supported as trailing x_x_x_Scrp) if (android.os.Build.VERSION.SDK_INT >= 21) { try { newLocale0 = new Locale.Builder().setLanguage(lsplit[0]).setScript(lsplit[3]).setRegion(lsplit[1]).setVariant(lsplit[2]).build(); } catch (RuntimeException e) { // Falls back to constructor } } final Locale newLocale = newLocale0; mTts = new TextToSpeech(ctx, new OnInitListener() { @Override public void onInit(int status) { if (status != TextToSpeech.SUCCESS) { ttsVoiceName = "NO INIT SUCCESS"; internalClear(); } else if (mTts != null) { speechAllowed = true; switch (mTts.isLanguageAvailable(newLocale)) { case TextToSpeech.LANG_MISSING_DATA: if (isSettingsActivity(act)) { AlertDialog.Builder builder = createAlertDialog( R.string.tts_missing_language_data_title, R.string.tts_missing_language_data, new IntentStarter( act, TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA), act); builder.show(); } ttsVoiceName = newLocale.getDisplayName() + ": LANG_MISSING_DATA"; ttsVoiceName = ttsVoiceName + "\n\n" + getVoiceUsed(); break; case TextToSpeech.LANG_AVAILABLE: ttsVoiceName = newLocale.getDisplayName() + ": LANG_AVAILABLE"; case TextToSpeech.LANG_COUNTRY_AVAILABLE: ttsVoiceName = "".equals(ttsVoiceName) ? newLocale.getDisplayName() + ": LANG_COUNTRY_AVAILABLE" : ttsVoiceName; case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: mTts.setLanguage(newLocale); if(speechRate != 1) { mTts.setSpeechRate(speechRate); } ttsVoiceName = "".equals(ttsVoiceName) ? newLocale.getDisplayName() + ": LANG_COUNTRY_VAR_AVAILABLE" : ttsVoiceName; ttsVoiceName = ttsVoiceName + "\n\n" + getVoiceUsed(); break; case TextToSpeech.LANG_NOT_SUPPORTED: //maybe weird, but I didn't want to introduce parameter in around 5 methods just to do this if condition if (isSettingsActivity(act)) { AlertDialog.Builder builder = createAlertDialog( R.string.tts_language_not_supported_title, R.string.tts_language_not_supported, new IntentStarter( act, Intent.ACTION_VIEW, Uri.parse("market://search?q=text to speech engine" )), act); builder.show(); } ttsVoiceName = newLocale.getDisplayName() + ": LANG_NOT_SUPPORTED"; ttsVoiceName = ttsVoiceName + "\n\n" + getVoiceUsed(); break; } } } private boolean isSettingsActivity(final Context ctx) { return ctx instanceof SettingsActivity; } private String getVoiceUsed() { try { if (android.os.Build.VERSION.SDK_INT >= 21) { if (mTts.getVoice() != null) { return mTts.getVoice().toString(); } } else { return mTts.getLanguage() + " (Voice details not reported in API<21)"; } } catch (RuntimeException e) { // mTts.getVoice() might throw NPE } return ""; } }); mTts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener() { // The call back is on a binder thread. @Override public synchronized void onUtteranceCompleted(String utteranceId) { if (--ttsRequests <= 0) abandonAudioFocus(); log.debug("ttsRequests="+ttsRequests); if (ttsRequests < 0) { ttsRequests = 0; } } }); } } public static String getTtsVoiceName() { return ttsVoiceName; } private AlertDialog.Builder createAlertDialog(int titleResID, int messageResID, IntentStarter intentStarter, final Activity ctx) { AlertDialog.Builder builder = new AlertDialog.Builder(ctx); builder.setCancelable(true); builder.setNegativeButton(R.string.shared_string_no, null); builder.setPositiveButton(R.string.shared_string_yes, intentStarter); builder.setTitle(titleResID); builder.setMessage(messageResID); return builder; } private void internalClear() { ttsRequests = 0; speechAllowed = false; if (mTts != null) { mTts.shutdown(); mTts = null; } abandonAudioFocus(); mTtsContext = null; ttsVoiceName = ""; } @Override public void clear() { super.clear(); internalClear(); } public static boolean isMyData(File voiceDir) { return new File(voiceDir, CONFIG_FILE).exists(); } @Override public void updateAudioStream(int streamType) { super.updateAudioStream(streamType); params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, streamType+""); } @Override public boolean supportsStructuredStreetNames() { return getCurrentVersion() >= 103; } }