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