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