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