• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.tv.settings.system;
18 
19 import static com.android.tv.settings.util.InstrumentationUtils.logEntrySelected;
20 
21 import android.app.AlertDialog;
22 import android.app.tvsettings.TvSettingsEnums;
23 import android.content.ActivityNotFoundException;
24 import android.content.ContentResolver;
25 import android.content.Intent;
26 import android.os.Bundle;
27 import android.provider.Settings;
28 import android.speech.tts.TextToSpeech;
29 import android.speech.tts.TtsEngines;
30 import android.speech.tts.UtteranceProgressListener;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.widget.Checkable;
34 
35 import androidx.annotation.Keep;
36 import androidx.preference.ListPreference;
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceCategory;
39 
40 import com.android.tv.settings.R;
41 import com.android.tv.settings.SettingsPreferenceFragment;
42 
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.MissingResourceException;
48 import java.util.Objects;
49 import java.util.Set;
50 
51 /**
52  * Fragment for TextToSpeech settings
53  */
54 @Keep
55 public class TextToSpeechFragment extends SettingsPreferenceFragment implements
56         Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
57         TtsEnginePreference.RadioButtonGroupState {
58     private static final String TAG = "TextToSpeechSettings";
59     private static final boolean DBG = false;
60 
61     /** Preference key for the engine settings preference */
62     private static final String KEY_ENGINE_SETTINGS = "tts_engine_settings";
63 
64     /** Preference key for the "play TTS example" preference. */
65     private static final String KEY_PLAY_EXAMPLE = "tts_play_example";
66 
67     /** Preference key for the TTS rate selection dialog. */
68     private static final String KEY_DEFAULT_RATE = "tts_default_rate";
69 
70     /** Preference key for the TTS status field. */
71     private static final String KEY_STATUS = "tts_status";
72 
73     /**
74      * Preference key for the engine selection preference.
75      */
76     private static final String KEY_ENGINE_PREFERENCE_SECTION =
77             "tts_engine_preference_section";
78 
79     /**
80      * These look like birth years, but they aren't mine. I'm much younger than this.
81      */
82     private static final int GET_SAMPLE_TEXT = 1983;
83     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
84 
85     private PreferenceCategory mEnginePreferenceCategory;
86     private Preference mEngineSettingsPref;
87     private ListPreference mDefaultRatePref;
88     private Preference mPlayExample;
89     private Preference mEngineStatus;
90 
91     private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
92 
93     /**
94      * The currently selected engine.
95      */
96     private String mCurrentEngine;
97 
98     /**
99      * The engine checkbox that is currently checked. Saves us a bit of effort
100      * in deducing the right one from the currently selected engine.
101      */
102     private Checkable mCurrentChecked;
103 
104     /**
105      * The previously selected TTS engine. Useful for rollbacks if the users
106      * choice is not loaded or fails a voice integrity check.
107      */
108     private String mPreviousEngine;
109 
110     private TextToSpeech mTts = null;
111     private TtsEngines mEnginesHelper = null;
112 
113     private String mSampleText = null;
114 
115     /**
116      * Default locale used by selected TTS engine, null if not connected to any engine.
117      */
118     private Locale mCurrentDefaultLocale;
119 
120     /**
121      * List of available locals of selected TTS engine, as returned by
122      * {@link TextToSpeech.Engine#ACTION_CHECK_TTS_DATA} activity. If empty, then activity
123      * was not yet called.
124      */
125     private List<String> mAvailableStrLocals;
126 
127     /**
128      * The initialization listener used when we are initalizing the settings
129      * screen for the first time (as opposed to when a user changes his choice
130      * of engine).
131      */
132     private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() {
133         @Override
134         public void onInit(int status) {
135             onInitEngine(status);
136         }
137     };
138 
139     /**
140      * The initialization listener used when the user changes his choice of
141      * engine (as opposed to when then screen is being initialized for the first
142      * time).
143      */
144     private final TextToSpeech.OnInitListener mUpdateListener = new TextToSpeech.OnInitListener() {
145         @Override
146         public void onInit(int status) {
147             onUpdateEngine(status);
148         }
149     };
150 
151     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)152     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
153         addPreferencesFromResource(R.xml.tts_settings);
154 
155         mEngineSettingsPref = findPreference(KEY_ENGINE_SETTINGS);
156 
157         mPlayExample = findPreference(KEY_PLAY_EXAMPLE);
158         mPlayExample.setOnPreferenceClickListener(this);
159         mPlayExample.setEnabled(false);
160 
161         mEnginePreferenceCategory = (PreferenceCategory) findPreference(
162                 KEY_ENGINE_PREFERENCE_SECTION);
163         mDefaultRatePref = (ListPreference) findPreference(KEY_DEFAULT_RATE);
164 
165         mEngineStatus = findPreference(KEY_STATUS);
166         updateEngineStatus(R.string.tts_status_checking);
167     }
168 
169     @Override
onCreate(Bundle savedInstanceState)170     public void onCreate(Bundle savedInstanceState) {
171         super.onCreate(savedInstanceState);
172 
173         getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
174 
175         mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
176         mEnginesHelper = new TtsEngines(getActivity().getApplicationContext());
177 
178         setTtsUtteranceProgressListener();
179         initSettings();
180     }
181 
182     @Override
onResume()183     public void onResume() {
184         super.onResume();
185 
186         if (mTts == null || mCurrentDefaultLocale == null) {
187             return;
188         }
189         Locale ttsDefaultLocale = mTts.getDefaultLanguage();
190         if (!mCurrentDefaultLocale.equals(ttsDefaultLocale)) {
191             updateWidgetState(false);
192             checkDefaultLocale();
193         }
194     }
195 
setTtsUtteranceProgressListener()196     private void setTtsUtteranceProgressListener() {
197         if (mTts == null) {
198             return;
199         }
200         mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
201             @Override
202             public void onStart(String utteranceId) {}
203 
204             @Override
205             public void onDone(String utteranceId) {}
206 
207             @Override
208             public void onError(String utteranceId) {
209                 Log.e(TAG, "Error while trying to synthesize sample text");
210             }
211         });
212     }
213 
214     @Override
onDestroy()215     public void onDestroy() {
216         super.onDestroy();
217         if (mTts != null) {
218             mTts.shutdown();
219             mTts = null;
220         }
221     }
222 
initSettings()223     private void initSettings() {
224         final ContentResolver resolver = getActivity().getContentResolver();
225 
226         // Set up the default rate.
227         try {
228             mDefaultRate = android.provider.Settings.Secure.getInt(resolver,
229                     Settings.Secure.TTS_DEFAULT_RATE);
230         } catch (Settings.SettingNotFoundException e) {
231             // Default rate setting not found, initialize it
232             mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
233         }
234         mDefaultRatePref.setValue(String.valueOf(mDefaultRate));
235         mDefaultRatePref.setOnPreferenceChangeListener(this);
236 
237         mCurrentEngine = mTts.getCurrentEngine();
238 
239         mEnginePreferenceCategory.removeAll();
240 
241         List<TextToSpeech.EngineInfo> engines = mEnginesHelper.getEngines();
242         for (TextToSpeech.EngineInfo engine : engines) {
243             TtsEnginePreference enginePref =
244                     new TtsEnginePreference(getPreferenceManager().getContext(), engine,
245                     this);
246             mEnginePreferenceCategory.addPreference(enginePref);
247         }
248 
249         checkVoiceData(mCurrentEngine);
250     }
251 
252     /**
253      * Called when the TTS engine is initialized.
254      */
onInitEngine(int status)255     public void onInitEngine(int status) {
256         if (mTts == null) {
257             return;
258         }
259         if (status == TextToSpeech.SUCCESS) {
260             if (DBG) Log.d(TAG, "TTS engine for settings screen initialized.");
261             checkDefaultLocale();
262         } else {
263             if (DBG) Log.d(TAG, "TTS engine for settings screen failed to initialize successfully.");
264             updateWidgetState(false);
265         }
266     }
267 
checkDefaultLocale()268     private void checkDefaultLocale() {
269         Locale defaultLocale = mTts.getDefaultLanguage();
270         if (defaultLocale == null) {
271             Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine);
272             updateWidgetState(false);
273             updateEngineStatus(R.string.tts_status_not_supported);
274             return;
275         }
276 
277         // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize,
278         // we may end up with English (USA)and German (DEU).
279         final Locale oldDefaultLocale = mCurrentDefaultLocale;
280         mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString());
281         if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) {
282             mSampleText = null;
283         }
284 
285         mTts.setLanguage(defaultLocale);
286         if (evaluateDefaultLocale() && mSampleText == null) {
287             getSampleText();
288         }
289     }
290 
evaluateDefaultLocale()291     private boolean evaluateDefaultLocale() {
292         // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list
293         // of available languages.
294         if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) {
295             return false;
296         }
297 
298         boolean notInAvailableLangauges = true;
299         try {
300             // Check if language is listed in CheckVoices Action result as available voice.
301             String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language();
302             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) {
303                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country();
304             }
305             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) {
306                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant();
307             }
308 
309             for (String loc : mAvailableStrLocals) {
310                 if (loc.equalsIgnoreCase(defaultLocaleStr)) {
311                     notInAvailableLangauges = false;
312                     break;
313                 }
314             }
315         } catch (MissingResourceException e) {
316             if (DBG) Log.wtf(TAG, "MissingResourceException", e);
317             updateEngineStatus(R.string.tts_status_not_supported);
318             updateWidgetState(false);
319             return false;
320         }
321 
322         int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale);
323         if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED ||
324                 defaultAvailable == TextToSpeech.LANG_MISSING_DATA ||
325                 notInAvailableLangauges) {
326             if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported.");
327             updateEngineStatus(R.string.tts_status_not_supported);
328             updateWidgetState(false);
329             return false;
330         } else {
331             if (isNetworkRequiredForSynthesis()) {
332                 updateEngineStatus(R.string.tts_status_requires_network);
333             } else {
334                 updateEngineStatus(R.string.tts_status_ok);
335             }
336             updateWidgetState(true);
337             return true;
338         }
339     }
340 
341     /**
342      * Ask the current default engine to return a string of sample text to be
343      * spoken to the user.
344      */
getSampleText()345     private void getSampleText() {
346         String currentEngine = mTts.getCurrentEngine();
347 
348         if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine();
349 
350         // TODO: This is currently a hidden private API. The intent extras
351         // and the intent action should be made public if we intend to make this
352         // a public API. We fall back to using a canned set of strings if this
353         // doesn't work.
354         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
355 
356         intent.putExtra("language", mCurrentDefaultLocale.getLanguage());
357         intent.putExtra("country", mCurrentDefaultLocale.getCountry());
358         intent.putExtra("variant", mCurrentDefaultLocale.getVariant());
359         intent.setPackage(currentEngine);
360 
361         try {
362             if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0));
363             startActivityForResult(intent, GET_SAMPLE_TEXT);
364         } catch (ActivityNotFoundException ex) {
365             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
366         }
367     }
368 
369     /**
370      * Called when voice data integrity check returns
371      */
372     @Override
onActivityResult(int requestCode, int resultCode, Intent data)373     public void onActivityResult(int requestCode, int resultCode, Intent data) {
374         if (requestCode == GET_SAMPLE_TEXT) {
375             onSampleTextReceived(resultCode, data);
376         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
377             onVoiceDataIntegrityCheckDone(data);
378         }
379     }
380 
getDefaultSampleString()381     private String getDefaultSampleString() {
382         if (mTts != null && mTts.getLanguage() != null) {
383             try {
384                 final String currentLang = mTts.getLanguage().getISO3Language();
385                 String[] strings = getActivity().getResources().getStringArray(
386                         R.array.tts_demo_strings);
387                 String[] langs = getActivity().getResources().getStringArray(
388                         R.array.tts_demo_string_langs);
389 
390                 for (int i = 0; i < strings.length; ++i) {
391                     if (langs[i].equals(currentLang)) {
392                         return strings[i];
393                     }
394                 }
395             } catch (MissingResourceException e) {
396                 if (DBG) Log.wtf(TAG, "MissingResourceException", e);
397                 // Ignore and fall back to default sample string
398             }
399         }
400         return getString(R.string.tts_default_sample_string);
401     }
402 
isNetworkRequiredForSynthesis()403     private boolean isNetworkRequiredForSynthesis() {
404         Set<String> features = mTts.getFeatures(mCurrentDefaultLocale);
405         return features != null &&
406                 features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
407                 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
408     }
409 
onSampleTextReceived(int resultCode, Intent data)410     private void onSampleTextReceived(int resultCode, Intent data) {
411         String sample = getDefaultSampleString();
412 
413         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
414             if (data.getStringExtra("sampleText") != null) {
415                 sample = data.getStringExtra("sampleText");
416             }
417             if (DBG) Log.d(TAG, "Got sample text: " + sample);
418         } else {
419             if (DBG) Log.d(TAG, "Using default sample text :" + sample);
420         }
421 
422         mSampleText = sample;
423         if (mSampleText != null) {
424             updateWidgetState(true);
425         } else {
426             Log.e(TAG, "Did not have a sample string for the requested language. Using default");
427         }
428     }
429 
speakSampleText()430     private void speakSampleText() {
431         final boolean networkRequired = isNetworkRequiredForSynthesis();
432         if (!networkRequired ||
433                 mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE) {
434             HashMap<String, String> params = new HashMap<>();
435             params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample");
436 
437             mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params);
438         } else {
439             Log.w(TAG, "Network required for sample synthesis for requested language");
440             displayNetworkAlert();
441         }
442     }
443 
444     @Override
onPreferenceChange(Preference preference, Object objValue)445     public boolean onPreferenceChange(Preference preference, Object objValue) {
446         if (KEY_DEFAULT_RATE.equals(preference.getKey())) {
447             logEntrySelected(TvSettingsEnums.SYSTEM_A11Y_TTS_SPEECH_RATE);
448             // Default rate
449             mDefaultRate = Integer.parseInt((String) objValue);
450             try {
451                 android.provider.Settings.Secure.putInt(getActivity().getContentResolver(),
452                         Settings.Secure.TTS_DEFAULT_RATE, mDefaultRate);
453                 if (mTts != null) {
454                     mTts.setSpeechRate(mDefaultRate / 100.0f);
455                 }
456                 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate);
457             } catch (NumberFormatException e) {
458                 Log.e(TAG, "could not persist default TTS rate setting", e);
459             }
460         }
461 
462         return true;
463     }
464 
465     /**
466      * Called when mPlayExample is clicked
467      */
468     @Override
onPreferenceClick(Preference preference)469     public boolean onPreferenceClick(Preference preference) {
470         if (preference == mPlayExample) {
471             logEntrySelected(TvSettingsEnums.SYSTEM_A11Y_TTS_LISTEN_EXAMPLE);
472             // Get the sample text from the TTS engine; onActivityResult will do
473             // the actual speaking
474             speakSampleText();
475             return true;
476         }
477 
478         return false;
479     }
480 
updateWidgetState(boolean enable)481     private void updateWidgetState(boolean enable) {
482         mPlayExample.setEnabled(enable);
483         mDefaultRatePref.setEnabled(enable);
484         mEngineStatus.setEnabled(enable);
485     }
486 
updateEngineStatus(int resourceId)487     private void updateEngineStatus(int resourceId) {
488         Locale locale = mCurrentDefaultLocale;
489         if (locale == null) {
490             locale = Locale.getDefault();
491         }
492         mEngineStatus.setSummary(getString(resourceId, locale.getDisplayName()));
493     }
494 
displayNetworkAlert()495     private void displayNetworkAlert() {
496         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
497         builder.setTitle(android.R.string.dialog_alert_title)
498                 .setMessage(getActivity().getString(R.string.tts_engine_network_required))
499                 .setCancelable(false)
500                 .setPositiveButton(android.R.string.ok, null);
501 
502         AlertDialog dialog = builder.create();
503         dialog.show();
504     }
505 
updateDefaultEngine(String engine)506     private void updateDefaultEngine(String engine) {
507         if (DBG) Log.d(TAG, "Updating default synth to : " + engine);
508 
509         // Disable the "play sample text" preference and the speech
510         // rate preference while the engine is being swapped.
511         updateWidgetState(false);
512         updateEngineStatus(R.string.tts_status_checking);
513 
514         // Keep track of the previous engine that was being used. So that
515         // we can reuse the previous engine.
516         //
517         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
518         // the very least that we successfully bound to the engine service.
519         mPreviousEngine = mTts.getCurrentEngine();
520 
521         // Step 1: Shut down the existing TTS engine.
522         try {
523             mTts.shutdown();
524             mTts = null;
525         } catch (Exception e) {
526             Log.e(TAG, "Error shutting down TTS engine" + e);
527         }
528 
529         // Step 2: Connect to the new TTS engine.
530         // Step 3 is continued on #onUpdateEngine (below) which is called when
531         // the app binds successfully to the engine.
532         if (DBG) Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine);
533         mTts = new TextToSpeech(getActivity().getApplicationContext(), mUpdateListener, engine);
534         setTtsUtteranceProgressListener();
535     }
536 
537     /*
538      * Step 3: We have now bound to the TTS engine the user requested. We will
539      * attempt to check voice data for the engine if we successfully bound to it,
540      * or revert to the previous engine if we didn't.
541      */
onUpdateEngine(int status)542     public void onUpdateEngine(int status) {
543         if (status == TextToSpeech.SUCCESS) {
544             if (DBG) {
545                 Log.d(TAG, "Updating engine: Successfully bound to the engine: " +
546                         mTts.getCurrentEngine());
547             }
548             checkVoiceData(mTts.getCurrentEngine());
549         } else {
550             if (DBG) Log.d(TAG, "Updating engine: Failed to bind to engine, reverting.");
551             if (mPreviousEngine != null) {
552                 // This is guaranteed to at least bind, since mPreviousEngine would be
553                 // null if the previous bind to this engine failed.
554                 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener,
555                         mPreviousEngine);
556                 setTtsUtteranceProgressListener();
557             }
558             mPreviousEngine = null;
559         }
560     }
561 
562     /*
563      * Step 4: Check whether the voice data for the engine is ok.
564      */
checkVoiceData(String engine)565     private void checkVoiceData(String engine) {
566         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
567         intent.setPackage(engine);
568         try {
569             if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
570             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
571         } catch (ActivityNotFoundException ex) {
572             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
573         }
574     }
575 
576     /*
577      * Step 5: The voice data check is complete.
578      */
onVoiceDataIntegrityCheckDone(Intent data)579     private void onVoiceDataIntegrityCheckDone(Intent data) {
580         final String engine = mTts.getCurrentEngine();
581 
582         if (engine == null) {
583             Log.e(TAG, "Voice data check complete, but no engine bound");
584             return;
585         }
586 
587         if (data == null){
588             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
589                     mTts.getCurrentEngine());
590             return;
591         }
592 
593         android.provider.Settings.Secure.putString(getActivity().getContentResolver(),
594                 Settings.Secure.TTS_DEFAULT_SYNTH, engine);
595 
596         mAvailableStrLocals = data.getStringArrayListExtra(
597                 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
598         if (mAvailableStrLocals == null) {
599             Log.e(TAG, "Voice data check complete, but no available voices found");
600             // Set mAvailableStrLocals to empty list
601             mAvailableStrLocals = new ArrayList<>();
602         }
603         if (evaluateDefaultLocale()) {
604             getSampleText();
605         }
606 
607         final TextToSpeech.EngineInfo engineInfo = mEnginesHelper.getEngineInfo(engine);
608         TtsEngineSettingsFragment.prepareArgs(mEngineSettingsPref.getExtras(),
609                 engineInfo.name, engineInfo.label, data);
610     }
611 
612     @Override
getCurrentChecked()613     public Checkable getCurrentChecked() {
614         return mCurrentChecked;
615     }
616 
617     @Override
getCurrentKey()618     public String getCurrentKey() {
619         return mCurrentEngine;
620     }
621 
622     @Override
setCurrentChecked(Checkable current)623     public void setCurrentChecked(Checkable current) {
624         mCurrentChecked = current;
625     }
626 
627     @Override
setCurrentKey(String key)628     public void setCurrentKey(String key) {
629         mCurrentEngine = key;
630         updateDefaultEngine(mCurrentEngine);
631     }
632 
633     @Override
getPageId()634     protected int getPageId() {
635         return TvSettingsEnums.SYSTEM_A11Y_TTS;
636     }
637 }
638