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