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