1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.tts;
18 
19 import static android.provider.Settings.Secure.TTS_DEFAULT_PITCH;
20 import static android.provider.Settings.Secure.TTS_DEFAULT_RATE;
21 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
22 
23 import android.app.settings.SettingsEnums;
24 import android.content.ActivityNotFoundException;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.os.Bundle;
29 import android.provider.SearchIndexableResource;
30 import android.speech.tts.TextToSpeech;
31 import android.speech.tts.TextToSpeech.EngineInfo;
32 import android.speech.tts.TtsEngines;
33 import android.speech.tts.UtteranceProgressListener;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.util.Pair;
37 
38 import androidx.appcompat.app.AlertDialog;
39 import androidx.preference.ListPreference;
40 import androidx.preference.Preference;
41 
42 import com.android.settings.R;
43 import com.android.settings.SettingsActivity;
44 import com.android.settings.SettingsPreferenceFragment;
45 import com.android.settings.search.BaseSearchIndexProvider;
46 import com.android.settings.search.Indexable;
47 import com.android.settings.widget.GearPreference;
48 import com.android.settings.widget.SeekBarPreference;
49 import com.android.settingslib.search.SearchIndexable;
50 import com.android.settingslib.widget.ActionButtonsPreference;
51 
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.MissingResourceException;
59 import java.util.Objects;
60 import java.util.Set;
61 
62 @SearchIndexable
63 public class TextToSpeechSettings extends SettingsPreferenceFragment
64         implements Preference.OnPreferenceChangeListener,
65         GearPreference.OnGearClickListener {
66 
67     private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries";
68     private static final String STATE_KEY_LOCALE_ENTRY_VALUES = "locale_entry_values";
69     private static final String STATE_KEY_LOCALE_VALUE = "locale_value";
70 
71     private static final String TAG = "TextToSpeechSettings";
72     private static final boolean DBG = false;
73 
74     /** Preference key for the TTS pitch selection slider. */
75     private static final String KEY_DEFAULT_PITCH = "tts_default_pitch";
76 
77     /** Preference key for the TTS rate selection slider. */
78     private static final String KEY_DEFAULT_RATE = "tts_default_rate";
79 
80     /** Engine picker. */
81     private static final String KEY_TTS_ENGINE_PREFERENCE = "tts_engine_preference";
82 
83     /** Locale picker. */
84     private static final String KEY_ENGINE_LOCALE = "tts_default_lang";
85 
86     /** Play/Reset buttons container. */
87     private static final String KEY_ACTION_BUTTONS = "action_buttons";
88 
89     /**
90      * These look like birth years, but they aren't mine. I'm much younger than this.
91      */
92     private static final int GET_SAMPLE_TEXT = 1983;
93     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
94 
95     /**
96      * Speech rate value. This value should be kept in sync with the max value set in tts_settings
97      * xml.
98      */
99     private static final int MAX_SPEECH_RATE = 600;
100 
101     private static final int MIN_SPEECH_RATE = 10;
102 
103     /**
104      * Speech pitch value. TTS pitch value varies from 25 to 400, where 100 is the value for normal
105      * pitch. The max pitch value is set to 400, based on feedback from users and the GoogleTTS
106      * pitch variation range. The range for pitch is not set in stone and should be readjusted based
107      * on user need. This value should be kept in sync with the max value set in tts_settings xml.
108      */
109     private static final int MAX_SPEECH_PITCH = 400;
110 
111     private static final int MIN_SPEECH_PITCH = 25;
112 
113     private SeekBarPreference mDefaultPitchPref;
114     private SeekBarPreference mDefaultRatePref;
115     private ActionButtonsPreference mActionButtons;
116 
117     private int mDefaultPitch = TextToSpeech.Engine.DEFAULT_PITCH;
118     private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
119 
120     private int mSelectedLocaleIndex = -1;
121 
122     /** The currently selected engine. */
123     private String mCurrentEngine;
124 
125     private TextToSpeech mTts = null;
126     private TtsEngines mEnginesHelper = null;
127 
128     private String mSampleText = null;
129 
130     private ListPreference mLocalePreference;
131 
132     /**
133      * Default locale used by selected TTS engine, null if not connected to any engine.
134      */
135     private Locale mCurrentDefaultLocale;
136 
137     /**
138      * List of available locals of selected TTS engine, as returned by
139      * {@link TextToSpeech.Engine#ACTION_CHECK_TTS_DATA} activity. If empty, then activity
140      * was not yet called.
141      */
142     private List<String> mAvailableStrLocals;
143 
144     /**
145      * The initialization listener used when we are initalizing the settings
146      * screen for the first time (as opposed to when a user changes his choice
147      * of engine).
148      */
149     private final TextToSpeech.OnInitListener mInitListener = this::onInitEngine;
150 
151     @Override
getMetricsCategory()152     public int getMetricsCategory() {
153         return SettingsEnums.TTS_TEXT_TO_SPEECH;
154     }
155 
156     @Override
onCreate(Bundle savedInstanceState)157     public void onCreate(Bundle savedInstanceState) {
158         super.onCreate(savedInstanceState);
159         addPreferencesFromResource(R.xml.tts_settings);
160 
161         getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
162 
163         mEnginesHelper = new TtsEngines(getActivity().getApplicationContext());
164 
165         mLocalePreference = (ListPreference) findPreference(KEY_ENGINE_LOCALE);
166         mLocalePreference.setOnPreferenceChangeListener(this);
167 
168         mDefaultPitchPref = (SeekBarPreference) findPreference(KEY_DEFAULT_PITCH);
169         mDefaultRatePref = (SeekBarPreference) findPreference(KEY_DEFAULT_RATE);
170 
171         mActionButtons = ((ActionButtonsPreference) findPreference(KEY_ACTION_BUTTONS))
172                 .setButton1Text(R.string.tts_play)
173                 .setButton1OnClickListener(v -> speakSampleText())
174                 .setButton1Enabled(false)
175                 .setButton2Text(R.string.tts_reset)
176                 .setButton2OnClickListener(v -> resetTts())
177                 .setButton1Enabled(true);
178 
179         if (savedInstanceState == null) {
180             mLocalePreference.setEnabled(false);
181             mLocalePreference.setEntries(new CharSequence[0]);
182             mLocalePreference.setEntryValues(new CharSequence[0]);
183         } else {
184             // Repopulate mLocalePreference with saved state. Will be updated later with
185             // up-to-date values when checkTtsData() calls back with results.
186             final CharSequence[] entries =
187                     savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRIES);
188             final CharSequence[] entryValues =
189                     savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES);
190             final CharSequence value = savedInstanceState.getCharSequence(STATE_KEY_LOCALE_VALUE);
191 
192             mLocalePreference.setEntries(entries);
193             mLocalePreference.setEntryValues(entryValues);
194             mLocalePreference.setValue(value != null ? value.toString() : null);
195             mLocalePreference.setEnabled(entries.length > 0);
196         }
197 
198         mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
199 
200         setTtsUtteranceProgressListener();
201         initSettings();
202 
203         // Prevent restarting the TTS connection on rotation
204         setRetainInstance(true);
205     }
206 
207     @Override
onResume()208     public void onResume() {
209         super.onResume();
210         // We tend to change the summary contents of our widgets, which at higher text sizes causes
211         // them to resize, which results in the recyclerview smoothly animating them at inopportune
212         // times. Disable the animation so widgets snap to their positions rather than sliding
213         // around while the user is interacting with it.
214         getListView().getItemAnimator().setMoveDuration(0);
215 
216         if (mTts == null || mCurrentDefaultLocale == null) {
217             return;
218         }
219         if (!mTts.getDefaultEngine().equals(mTts.getCurrentEngine())) {
220             try {
221                 mTts.shutdown();
222                 mTts = null;
223             } catch (Exception e) {
224                 Log.e(TAG, "Error shutting down TTS engine" + e);
225             }
226             mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
227             setTtsUtteranceProgressListener();
228             initSettings();
229         } else {
230             // Do set pitch correctly after it may have changed, and unlike speed, it doesn't change
231             // immediately.
232             final ContentResolver resolver = getContentResolver();
233             mTts.setPitch(android.provider.Settings.Secure.getInt(resolver, TTS_DEFAULT_PITCH,
234                     TextToSpeech.Engine.DEFAULT_PITCH) / 100.0f);
235         }
236 
237         Locale ttsDefaultLocale = mTts.getDefaultLanguage();
238         if (mCurrentDefaultLocale != null && !mCurrentDefaultLocale.equals(ttsDefaultLocale)) {
239             updateWidgetState(false);
240             checkDefaultLocale();
241         }
242     }
243 
setTtsUtteranceProgressListener()244     private void setTtsUtteranceProgressListener() {
245         if (mTts == null) {
246             return;
247         }
248         mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
249             @Override
250             public void onStart(String utteranceId) {
251                 updateWidgetState(false);
252             }
253 
254             @Override
255             public void onDone(String utteranceId) {
256                 updateWidgetState(true);
257             }
258 
259             @Override
260             public void onError(String utteranceId) {
261                 Log.e(TAG, "Error while trying to synthesize sample text");
262                 // Re-enable just in case, although there isn't much hope that following synthesis
263                 // requests are going to succeed.
264                 updateWidgetState(true);
265             }
266         });
267     }
268 
269     @Override
onDestroy()270     public void onDestroy() {
271         super.onDestroy();
272         if (mTts != null) {
273             mTts.shutdown();
274             mTts = null;
275         }
276     }
277 
278     @Override
onSaveInstanceState(Bundle outState)279     public void onSaveInstanceState(Bundle outState) {
280         super.onSaveInstanceState(outState);
281 
282         // Save the mLocalePreference values, so we can repopulate it with entries.
283         outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRIES,
284                 mLocalePreference.getEntries());
285         outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES,
286                 mLocalePreference.getEntryValues());
287         outState.putCharSequence(STATE_KEY_LOCALE_VALUE,
288                 mLocalePreference.getValue());
289     }
290 
initSettings()291     private void initSettings() {
292         final ContentResolver resolver = getContentResolver();
293 
294         // Set up the default rate and pitch.
295         mDefaultRate =
296                 android.provider.Settings.Secure.getInt(
297                         resolver, TTS_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE);
298         mDefaultPitch =
299                 android.provider.Settings.Secure.getInt(
300                         resolver, TTS_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH);
301 
302         mDefaultRatePref.setProgress(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, mDefaultRate));
303         mDefaultRatePref.setOnPreferenceChangeListener(this);
304         mDefaultRatePref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, MAX_SPEECH_RATE));
305 
306         mDefaultPitchPref.setProgress(
307                 getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, mDefaultPitch));
308         mDefaultPitchPref.setOnPreferenceChangeListener(this);
309         mDefaultPitchPref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, MAX_SPEECH_PITCH));
310 
311         if (mTts != null) {
312             mCurrentEngine = mTts.getCurrentEngine();
313             mTts.setSpeechRate(mDefaultRate / 100.0f);
314             mTts.setPitch(mDefaultPitch / 100.0f);
315         }
316 
317         SettingsActivity activity = null;
318         if (getActivity() instanceof SettingsActivity) {
319             activity = (SettingsActivity) getActivity();
320         } else {
321             throw new IllegalStateException("TextToSpeechSettings used outside a " +
322                     "Settings");
323         }
324 
325         if (mCurrentEngine != null) {
326             EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine);
327 
328             Preference mEnginePreference = findPreference(KEY_TTS_ENGINE_PREFERENCE);
329             ((GearPreference) mEnginePreference).setOnGearClickListener(this);
330             mEnginePreference.setSummary(info.label);
331         }
332 
333         checkVoiceData(mCurrentEngine);
334     }
335 
336     /**
337      * The minimum speech pitch/rate value should be > 0 but the minimum value of a seekbar in
338      * android is fixed at 0. Therefore, we increment the seekbar progress with MIN_SPEECH_VALUE so
339      * that the minimum seekbar progress value is MIN_SPEECH_PITCH/RATE. SPEECH_VALUE =
340      * MIN_SPEECH_VALUE + SEEKBAR_PROGRESS
341      */
getValueFromSeekBarProgress(String preferenceKey, int progress)342     private int getValueFromSeekBarProgress(String preferenceKey, int progress) {
343         if (preferenceKey.equals(KEY_DEFAULT_RATE)) {
344             return MIN_SPEECH_RATE + progress;
345         } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) {
346             return MIN_SPEECH_PITCH + progress;
347         }
348         return progress;
349     }
350 
351     /**
352      * Since we are appending the MIN_SPEECH value to the speech seekbar progress, the speech
353      * seekbar progress should be set to (speechValue - MIN_SPEECH value).
354      */
getSeekBarProgressFromValue(String preferenceKey, int value)355     private int getSeekBarProgressFromValue(String preferenceKey, int value) {
356         if (preferenceKey.equals(KEY_DEFAULT_RATE)) {
357             return value - MIN_SPEECH_RATE;
358         } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) {
359             return value - MIN_SPEECH_PITCH;
360         }
361         return value;
362     }
363 
364     /** Called when the TTS engine is initialized. */
onInitEngine(int status)365     public void onInitEngine(int status) {
366         if (status == TextToSpeech.SUCCESS) {
367             if (DBG) Log.d(TAG, "TTS engine for settings screen initialized.");
368             checkDefaultLocale();
369             getActivity().runOnUiThread(() -> mLocalePreference.setEnabled(true));
370         } else {
371             if (DBG) {
372                 Log.d(TAG,
373                         "TTS engine for settings screen failed to initialize successfully.");
374             }
375             updateWidgetState(false);
376         }
377     }
378 
checkDefaultLocale()379     private void checkDefaultLocale() {
380         Locale defaultLocale = mTts.getDefaultLanguage();
381         if (defaultLocale == null) {
382             Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine);
383             updateWidgetState(false);
384             return;
385         }
386 
387         // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize,
388         // we may end up with English (USA)and German (DEU).
389         final Locale oldDefaultLocale = mCurrentDefaultLocale;
390         mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString());
391         if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) {
392             mSampleText = null;
393         }
394 
395         int defaultAvailable = mTts.setLanguage(defaultLocale);
396         if (evaluateDefaultLocale() && mSampleText == null) {
397             getSampleText();
398         }
399     }
400 
evaluateDefaultLocale()401     private boolean evaluateDefaultLocale() {
402         // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list
403         // of available languages.
404         if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) {
405             return false;
406         }
407 
408         boolean notInAvailableLangauges = true;
409         try {
410             // Check if language is listed in CheckVoices Action result as available voice.
411             String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language();
412             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) {
413                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country();
414             }
415             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) {
416                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant();
417             }
418 
419             for (String loc : mAvailableStrLocals) {
420                 if (loc.equalsIgnoreCase(defaultLocaleStr)) {
421                     notInAvailableLangauges = false;
422                     break;
423                 }
424             }
425         } catch (MissingResourceException e) {
426             if (DBG) Log.wtf(TAG, "MissingResourceException", e);
427             updateWidgetState(false);
428             return false;
429         }
430 
431         int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale);
432         if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED ||
433                 defaultAvailable == TextToSpeech.LANG_MISSING_DATA ||
434                 notInAvailableLangauges) {
435             if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported.");
436             updateWidgetState(false);
437             return false;
438         } else {
439             updateWidgetState(true);
440             return true;
441         }
442     }
443 
444     /**
445      * Ask the current default engine to return a string of sample text to be
446      * spoken to the user.
447      */
getSampleText()448     private void getSampleText() {
449         String currentEngine = mTts.getCurrentEngine();
450 
451         if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine();
452 
453         // TODO: This is currently a hidden private API. The intent extras
454         // and the intent action should be made public if we intend to make this
455         // a public API. We fall back to using a canned set of strings if this
456         // doesn't work.
457         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
458 
459         intent.putExtra("language", mCurrentDefaultLocale.getLanguage());
460         intent.putExtra("country", mCurrentDefaultLocale.getCountry());
461         intent.putExtra("variant", mCurrentDefaultLocale.getVariant());
462         intent.setPackage(currentEngine);
463 
464         try {
465             if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0));
466             startActivityForResult(intent, GET_SAMPLE_TEXT);
467         } catch (ActivityNotFoundException ex) {
468             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
469         }
470     }
471 
472     /**
473      * Called when voice data integrity check returns
474      */
475     @Override
onActivityResult(int requestCode, int resultCode, Intent data)476     public void onActivityResult(int requestCode, int resultCode, Intent data) {
477         if (requestCode == GET_SAMPLE_TEXT) {
478             onSampleTextReceived(resultCode, data);
479         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
480             onVoiceDataIntegrityCheckDone(data);
481             if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) {
482                 updateDefaultLocalePref(data);
483             }
484         }
485     }
486 
updateDefaultLocalePref(Intent data)487     private void updateDefaultLocalePref(Intent data) {
488         final ArrayList<String> availableLangs =
489                 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
490 
491         final ArrayList<String> unavailableLangs =
492                 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES);
493 
494         if (availableLangs == null || availableLangs.size() == 0) {
495             mLocalePreference.setEnabled(false);
496             return;
497         }
498         Locale currentLocale = null;
499         if (!mEnginesHelper.isLocaleSetToDefaultForEngine(mTts.getCurrentEngine())) {
500             currentLocale = mEnginesHelper.getLocalePrefForEngine(mTts.getCurrentEngine());
501         }
502 
503         ArrayList<Pair<String, Locale>> entryPairs =
504                 new ArrayList<Pair<String, Locale>>(availableLangs.size());
505         for (int i = 0; i < availableLangs.size(); i++) {
506             Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i));
507             if (locale != null) {
508                 entryPairs.add(new Pair<String, Locale>(locale.getDisplayName(), locale));
509             }
510         }
511 
512         // Sort it
513         Collections.sort(entryPairs, (lhs, rhs) -> lhs.first.compareToIgnoreCase(rhs.first));
514 
515         // Get two arrays out of one of pairs
516         mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value
517         CharSequence[] entries = new CharSequence[availableLangs.size() + 1];
518         CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1];
519 
520         entries[0] = getActivity().getString(R.string.tts_lang_use_system);
521         entryValues[0] = "";
522 
523         int i = 1;
524         for (Pair<String, Locale> entry : entryPairs) {
525             if (entry.second.equals(currentLocale)) {
526                 mSelectedLocaleIndex = i;
527             }
528             entries[i] = entry.first;
529             entryValues[i++] = entry.second.toString();
530         }
531 
532         mLocalePreference.setEntries(entries);
533         mLocalePreference.setEntryValues(entryValues);
534         mLocalePreference.setEnabled(true);
535         setLocalePreference(mSelectedLocaleIndex);
536     }
537 
538     /** Set entry from entry table in mLocalePreference */
setLocalePreference(int index)539     private void setLocalePreference(int index) {
540         if (index < 0) {
541             mLocalePreference.setValue("");
542             mLocalePreference.setSummary(R.string.tts_lang_not_selected);
543         } else {
544             mLocalePreference.setValueIndex(index);
545             mLocalePreference.setSummary(mLocalePreference.getEntries()[index]);
546         }
547     }
548 
549 
getDefaultSampleString()550     private String getDefaultSampleString() {
551         if (mTts != null && mTts.getLanguage() != null) {
552             try {
553                 final String currentLang = mTts.getLanguage().getISO3Language();
554                 String[] strings = getActivity().getResources().getStringArray(
555                         R.array.tts_demo_strings);
556                 String[] langs = getActivity().getResources().getStringArray(
557                         R.array.tts_demo_string_langs);
558 
559                 for (int i = 0; i < strings.length; ++i) {
560                     if (langs[i].equals(currentLang)) {
561                         return strings[i];
562                     }
563                 }
564             } catch (MissingResourceException e) {
565                 if (DBG) Log.wtf(TAG, "MissingResourceException", e);
566                 // Ignore and fall back to default sample string
567             }
568         }
569         return getString(R.string.tts_default_sample_string);
570     }
571 
isNetworkRequiredForSynthesis()572     private boolean isNetworkRequiredForSynthesis() {
573         Set<String> features = mTts.getFeatures(mCurrentDefaultLocale);
574         if (features == null) {
575             return false;
576         }
577         return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
578                 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
579     }
580 
onSampleTextReceived(int resultCode, Intent data)581     private void onSampleTextReceived(int resultCode, Intent data) {
582         String sample = getDefaultSampleString();
583 
584         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
585             if (data != null && data.getStringExtra("sampleText") != null) {
586                 sample = data.getStringExtra("sampleText");
587             }
588             if (DBG) Log.d(TAG, "Got sample text: " + sample);
589         } else {
590             if (DBG) Log.d(TAG, "Using default sample text :" + sample);
591         }
592 
593         mSampleText = sample;
594         if (mSampleText != null) {
595             updateWidgetState(true);
596         } else {
597             Log.e(TAG, "Did not have a sample string for the requested language. Using default");
598         }
599     }
600 
speakSampleText()601     private void speakSampleText() {
602         final boolean networkRequired = isNetworkRequiredForSynthesis();
603         if (!networkRequired || networkRequired &&
604                 (mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE)) {
605             HashMap<String, String> params = new HashMap<String, String>();
606             params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample");
607 
608             mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params);
609         } else {
610             Log.w(TAG, "Network required for sample synthesis for requested language");
611             displayNetworkAlert();
612         }
613     }
614 
615     @Override
onPreferenceChange(Preference preference, Object objValue)616     public boolean onPreferenceChange(Preference preference, Object objValue) {
617         if (KEY_DEFAULT_RATE.equals(preference.getKey())) {
618             updateSpeechRate((Integer) objValue);
619         } else if (KEY_DEFAULT_PITCH.equals(preference.getKey())) {
620             updateSpeechPitchValue((Integer) objValue);
621         } else if (preference == mLocalePreference) {
622             String localeString = (String) objValue;
623             updateLanguageTo(
624                     (!TextUtils.isEmpty(localeString)
625                             ? mEnginesHelper.parseLocaleString(localeString)
626                             : null));
627             checkDefaultLocale();
628             return true;
629         }
630         return true;
631     }
632 
updateLanguageTo(Locale locale)633     private void updateLanguageTo(Locale locale) {
634         int selectedLocaleIndex = -1;
635         String localeString = (locale != null) ? locale.toString() : "";
636         for (int i = 0; i < mLocalePreference.getEntryValues().length; i++) {
637             if (localeString.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) {
638                 selectedLocaleIndex = i;
639                 break;
640             }
641         }
642 
643         if (selectedLocaleIndex == -1) {
644             Log.w(TAG, "updateLanguageTo called with unknown locale argument");
645             return;
646         }
647         mLocalePreference.setSummary(mLocalePreference.getEntries()[selectedLocaleIndex]);
648         mSelectedLocaleIndex = selectedLocaleIndex;
649 
650         mEnginesHelper.updateLocalePrefForEngine(mTts.getCurrentEngine(), locale);
651 
652         // Null locale means "use system default"
653         mTts.setLanguage((locale != null) ? locale : Locale.getDefault());
654     }
655 
resetTts()656     private void resetTts() {
657         // Reset button.
658         int speechRateSeekbarProgress =
659                 getSeekBarProgressFromValue(
660                         KEY_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE);
661         mDefaultRatePref.setProgress(speechRateSeekbarProgress);
662         updateSpeechRate(speechRateSeekbarProgress);
663         int pitchSeekbarProgress =
664                 getSeekBarProgressFromValue(
665                         KEY_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH);
666         mDefaultPitchPref.setProgress(pitchSeekbarProgress);
667         updateSpeechPitchValue(pitchSeekbarProgress);
668     }
669 
updateSpeechRate(int speechRateSeekBarProgress)670     private void updateSpeechRate(int speechRateSeekBarProgress) {
671         mDefaultRate = getValueFromSeekBarProgress(KEY_DEFAULT_RATE, speechRateSeekBarProgress);
672         try {
673             android.provider.Settings.Secure.putInt(
674                     getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate);
675             if (mTts != null) {
676                 mTts.setSpeechRate(mDefaultRate / 100.0f);
677             }
678             if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate);
679         } catch (NumberFormatException e) {
680             Log.e(TAG, "could not persist default TTS rate setting", e);
681         }
682         return;
683     }
684 
updateSpeechPitchValue(int speechPitchSeekBarProgress)685     private void updateSpeechPitchValue(int speechPitchSeekBarProgress) {
686         mDefaultPitch = getValueFromSeekBarProgress(KEY_DEFAULT_PITCH, speechPitchSeekBarProgress);
687         try {
688             android.provider.Settings.Secure.putInt(
689                     getContentResolver(), TTS_DEFAULT_PITCH, mDefaultPitch);
690             if (mTts != null) {
691                 mTts.setPitch(mDefaultPitch / 100.0f);
692             }
693             if (DBG) Log.d(TAG, "TTS default pitch changed, now" + mDefaultPitch);
694         } catch (NumberFormatException e) {
695             Log.e(TAG, "could not persist default TTS pitch setting", e);
696         }
697         return;
698     }
699 
updateWidgetState(boolean enable)700     private void updateWidgetState(boolean enable) {
701         getActivity().runOnUiThread(() -> {
702             mActionButtons.setButton1Enabled(enable);
703             mDefaultRatePref.setEnabled(enable);
704             mDefaultPitchPref.setEnabled(enable);
705         });
706     }
707 
displayNetworkAlert()708     private void displayNetworkAlert() {
709         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
710         builder.setTitle(android.R.string.dialog_alert_title)
711                 .setMessage(getActivity().getString(R.string.tts_engine_network_required))
712                 .setCancelable(false)
713                 .setPositiveButton(android.R.string.ok, null);
714 
715         AlertDialog dialog = builder.create();
716         dialog.show();
717     }
718 
719     /** Check whether the voice data for the engine is ok. */
checkVoiceData(String engine)720     private void checkVoiceData(String engine) {
721         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
722         intent.setPackage(engine);
723         try {
724             if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
725             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
726         } catch (ActivityNotFoundException ex) {
727             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
728         }
729     }
730 
731     /** The voice data check is complete. */
onVoiceDataIntegrityCheckDone(Intent data)732     private void onVoiceDataIntegrityCheckDone(Intent data) {
733         final String engine = mTts.getCurrentEngine();
734 
735         if (engine == null) {
736             Log.e(TAG, "Voice data check complete, but no engine bound");
737             return;
738         }
739 
740         if (data == null) {
741             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
742                     mTts.getCurrentEngine());
743             return;
744         }
745 
746         android.provider.Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine);
747 
748         mAvailableStrLocals = data.getStringArrayListExtra(
749                 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
750         if (mAvailableStrLocals == null) {
751             Log.e(TAG, "Voice data check complete, but no available voices found");
752             // Set mAvailableStrLocals to empty list
753             mAvailableStrLocals = new ArrayList<String>();
754         }
755         if (evaluateDefaultLocale()) {
756             getSampleText();
757         }
758     }
759 
760     @Override
onGearClick(GearPreference p)761     public void onGearClick(GearPreference p) {
762         if (KEY_TTS_ENGINE_PREFERENCE.equals(p.getKey())) {
763             EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine);
764             final Intent settingsIntent = mEnginesHelper.getSettingsIntent(info.name);
765             if (settingsIntent != null) {
766                 startActivity(settingsIntent);
767             } else {
768                 Log.e(TAG, "settingsIntent is null");
769             }
770         }
771     }
772 
773     public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
774             new BaseSearchIndexProvider() {
775                 @Override
776                 public List<SearchIndexableResource> getXmlResourcesToIndex(
777                         Context context, boolean enabled) {
778                     final SearchIndexableResource sir = new SearchIndexableResource(context);
779                     sir.xmlResId = R.xml.tts_settings;
780                     return Arrays.asList(sir);
781                 }
782             };
783 
784 }
785