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.settings.R; 23 import com.android.settings.SettingsActivity; 24 import com.android.settings.SettingsPreferenceFragment; 25 import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState; 26 27 import android.app.AlertDialog; 28 import android.content.ActivityNotFoundException; 29 import android.content.ContentResolver; 30 import android.content.Intent; 31 import android.os.AsyncTask; 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 onCreate(Bundle savedInstanceState)147 public void onCreate(Bundle savedInstanceState) { 148 super.onCreate(savedInstanceState); 149 addPreferencesFromResource(R.xml.tts_settings); 150 151 getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); 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 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener); 165 mEnginesHelper = new TtsEngines(getActivity().getApplicationContext()); 166 167 setTtsUtteranceProgressListener(); 168 initSettings(); 169 170 // Prevent restarting the TTS connection on rotation 171 setRetainInstance(true); 172 } 173 174 @Override onResume()175 public void onResume() { 176 super.onResume(); 177 178 if (mTts == null || mCurrentDefaultLocale == null) { 179 return; 180 } 181 Locale ttsDefaultLocale = mTts.getDefaultLanguage(); 182 if (mCurrentDefaultLocale != null && !mCurrentDefaultLocale.equals(ttsDefaultLocale)) { 183 updateWidgetState(false); 184 checkDefaultLocale(); 185 } 186 } 187 setTtsUtteranceProgressListener()188 private void setTtsUtteranceProgressListener() { 189 if (mTts == null) { 190 return; 191 } 192 mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() { 193 @Override 194 public void onStart(String utteranceId) {} 195 196 @Override 197 public void onDone(String utteranceId) {} 198 199 @Override 200 public void onError(String utteranceId) { 201 Log.e(TAG, "Error while trying to synthesize sample text"); 202 } 203 }); 204 } 205 206 @Override onDestroy()207 public void onDestroy() { 208 super.onDestroy(); 209 if (mTts != null) { 210 mTts.shutdown(); 211 mTts = null; 212 } 213 } 214 initSettings()215 private void initSettings() { 216 final ContentResolver resolver = getContentResolver(); 217 218 // Set up the default rate. 219 try { 220 mDefaultRate = android.provider.Settings.Secure.getInt(resolver, TTS_DEFAULT_RATE); 221 } catch (SettingNotFoundException e) { 222 // Default rate setting not found, initialize it 223 mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE; 224 } 225 mDefaultRatePref.setValue(String.valueOf(mDefaultRate)); 226 mDefaultRatePref.setOnPreferenceChangeListener(this); 227 228 mCurrentEngine = mTts.getCurrentEngine(); 229 230 SettingsActivity activity = null; 231 if (getActivity() instanceof SettingsActivity) { 232 activity = (SettingsActivity) getActivity(); 233 } else { 234 throw new IllegalStateException("TextToSpeechSettings used outside a " + 235 "Settings"); 236 } 237 238 mEnginePreferenceCategory.removeAll(); 239 240 List<EngineInfo> engines = mEnginesHelper.getEngines(); 241 for (EngineInfo engine : engines) { 242 TtsEnginePreference enginePref = new TtsEnginePreference(getActivity(), engine, 243 this, activity); 244 mEnginePreferenceCategory.addPreference(enginePref); 245 } 246 247 checkVoiceData(mCurrentEngine); 248 } 249 250 /** 251 * Called when the TTS engine is initialized. 252 */ onInitEngine(int status)253 public void onInitEngine(int status) { 254 if (status == TextToSpeech.SUCCESS) { 255 if (DBG) Log.d(TAG, "TTS engine for settings screen initialized."); 256 checkDefaultLocale(); 257 } else { 258 if (DBG) Log.d(TAG, "TTS engine for settings screen failed to initialize successfully."); 259 updateWidgetState(false); 260 } 261 } 262 checkDefaultLocale()263 private void checkDefaultLocale() { 264 Locale defaultLocale = mTts.getDefaultLanguage(); 265 if (defaultLocale == null) { 266 Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine); 267 updateWidgetState(false); 268 updateEngineStatus(R.string.tts_status_not_supported); 269 return; 270 } 271 272 // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize, 273 // we may end up with English (USA)and German (DEU). 274 final Locale oldDefaultLocale = mCurrentDefaultLocale; 275 mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString()); 276 if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) { 277 mSampleText = null; 278 } 279 280 int defaultAvailable = mTts.setLanguage(defaultLocale); 281 if (evaluateDefaultLocale() && mSampleText == null) { 282 getSampleText(); 283 } 284 } 285 evaluateDefaultLocale()286 private boolean evaluateDefaultLocale() { 287 // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list 288 // of available languages. 289 if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) { 290 return false; 291 } 292 293 boolean notInAvailableLangauges = true; 294 try { 295 // Check if language is listed in CheckVoices Action result as available voice. 296 String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language(); 297 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) { 298 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country(); 299 } 300 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) { 301 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant(); 302 } 303 304 for (String loc : mAvailableStrLocals) { 305 if (loc.equalsIgnoreCase(defaultLocaleStr)) { 306 notInAvailableLangauges = false; 307 break; 308 } 309 } 310 } catch (MissingResourceException e) { 311 if (DBG) Log.wtf(TAG, "MissingResourceException", e); 312 updateEngineStatus(R.string.tts_status_not_supported); 313 updateWidgetState(false); 314 return false; 315 } 316 317 int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale); 318 if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED || 319 defaultAvailable == TextToSpeech.LANG_MISSING_DATA || 320 notInAvailableLangauges) { 321 if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported."); 322 updateEngineStatus(R.string.tts_status_not_supported); 323 updateWidgetState(false); 324 return false; 325 } else { 326 if (isNetworkRequiredForSynthesis()) { 327 updateEngineStatus(R.string.tts_status_requires_network); 328 } else { 329 updateEngineStatus(R.string.tts_status_ok); 330 } 331 updateWidgetState(true); 332 return true; 333 } 334 } 335 336 /** 337 * Ask the current default engine to return a string of sample text to be 338 * spoken to the user. 339 */ getSampleText()340 private void getSampleText() { 341 String currentEngine = mTts.getCurrentEngine(); 342 343 if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine(); 344 345 // TODO: This is currently a hidden private API. The intent extras 346 // and the intent action should be made public if we intend to make this 347 // a public API. We fall back to using a canned set of strings if this 348 // doesn't work. 349 Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); 350 351 intent.putExtra("language", mCurrentDefaultLocale.getLanguage()); 352 intent.putExtra("country", mCurrentDefaultLocale.getCountry()); 353 intent.putExtra("variant", mCurrentDefaultLocale.getVariant()); 354 intent.setPackage(currentEngine); 355 356 try { 357 if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0)); 358 startActivityForResult(intent, GET_SAMPLE_TEXT); 359 } catch (ActivityNotFoundException ex) { 360 Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")"); 361 } 362 } 363 364 /** 365 * Called when voice data integrity check returns 366 */ 367 @Override onActivityResult(int requestCode, int resultCode, Intent data)368 public void onActivityResult(int requestCode, int resultCode, Intent data) { 369 if (requestCode == GET_SAMPLE_TEXT) { 370 onSampleTextReceived(resultCode, data); 371 } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { 372 onVoiceDataIntegrityCheckDone(data); 373 } 374 } 375 getDefaultSampleString()376 private String getDefaultSampleString() { 377 if (mTts != null && mTts.getLanguage() != null) { 378 try { 379 final String currentLang = mTts.getLanguage().getISO3Language(); 380 String[] strings = getActivity().getResources().getStringArray( 381 R.array.tts_demo_strings); 382 String[] langs = getActivity().getResources().getStringArray( 383 R.array.tts_demo_string_langs); 384 385 for (int i = 0; i < strings.length; ++i) { 386 if (langs[i].equals(currentLang)) { 387 return strings[i]; 388 } 389 } 390 } catch (MissingResourceException e) { 391 if (DBG) Log.wtf(TAG, "MissingResourceException", e); 392 // Ignore and fall back to default sample string 393 } 394 } 395 return getString(R.string.tts_default_sample_string); 396 } 397 isNetworkRequiredForSynthesis()398 private boolean isNetworkRequiredForSynthesis() { 399 Set<String> features = mTts.getFeatures(mCurrentDefaultLocale); 400 if (features == null) { 401 return false; 402 } 403 return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) && 404 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS); 405 } 406 onSampleTextReceived(int resultCode, Intent data)407 private void onSampleTextReceived(int resultCode, Intent data) { 408 String sample = getDefaultSampleString(); 409 410 if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) { 411 if (data != null && data.getStringExtra("sampleText") != null) { 412 sample = data.getStringExtra("sampleText"); 413 } 414 if (DBG) Log.d(TAG, "Got sample text: " + sample); 415 } else { 416 if (DBG) Log.d(TAG, "Using default sample text :" + sample); 417 } 418 419 mSampleText = sample; 420 if (mSampleText != null) { 421 updateWidgetState(true); 422 } else { 423 Log.e(TAG, "Did not have a sample string for the requested language. Using default"); 424 } 425 } 426 speakSampleText()427 private void speakSampleText() { 428 final boolean networkRequired = isNetworkRequiredForSynthesis(); 429 if (!networkRequired || networkRequired && 430 (mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE)) { 431 HashMap<String, String> params = new HashMap<String, String>(); 432 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample"); 433 434 mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params); 435 } else { 436 Log.w(TAG, "Network required for sample synthesis for requested language"); 437 displayNetworkAlert(); 438 } 439 } 440 441 @Override onPreferenceChange(Preference preference, Object objValue)442 public boolean onPreferenceChange(Preference preference, Object objValue) { 443 if (KEY_DEFAULT_RATE.equals(preference.getKey())) { 444 // Default rate 445 mDefaultRate = Integer.parseInt((String) objValue); 446 try { 447 android.provider.Settings.Secure.putInt(getContentResolver(), 448 TTS_DEFAULT_RATE, mDefaultRate); 449 if (mTts != null) { 450 mTts.setSpeechRate(mDefaultRate / 100.0f); 451 } 452 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate); 453 } catch (NumberFormatException e) { 454 Log.e(TAG, "could not persist default TTS rate setting", e); 455 } 456 } 457 458 return true; 459 } 460 461 /** 462 * Called when mPlayExample is clicked 463 */ 464 @Override onPreferenceClick(Preference preference)465 public boolean onPreferenceClick(Preference preference) { 466 if (preference == mPlayExample) { 467 // Get the sample text from the TTS engine; onActivityResult will do 468 // the actual speaking 469 speakSampleText(); 470 return true; 471 } 472 473 return false; 474 } 475 updateWidgetState(boolean enable)476 private void updateWidgetState(boolean enable) { 477 mPlayExample.setEnabled(enable); 478 mDefaultRatePref.setEnabled(enable); 479 mEngineStatus.setEnabled(enable); 480 } 481 updateEngineStatus(int resourceId)482 private void updateEngineStatus(int resourceId) { 483 Locale locale = mCurrentDefaultLocale; 484 if (locale == null) { 485 locale = Locale.getDefault(); 486 } 487 mEngineStatus.setSummary(getString(resourceId, locale.getDisplayName())); 488 } 489 displayNetworkAlert()490 private void displayNetworkAlert() { 491 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 492 builder.setTitle(android.R.string.dialog_alert_title) 493 .setMessage(getActivity().getString(R.string.tts_engine_network_required)) 494 .setCancelable(false) 495 .setPositiveButton(android.R.string.ok, null); 496 497 AlertDialog dialog = builder.create(); 498 dialog.show(); 499 } 500 updateDefaultEngine(String engine)501 private void updateDefaultEngine(String engine) { 502 if (DBG) Log.d(TAG, "Updating default synth to : " + engine); 503 504 // Disable the "play sample text" preference and the speech 505 // rate preference while the engine is being swapped. 506 updateWidgetState(false); 507 updateEngineStatus(R.string.tts_status_checking); 508 509 // Keep track of the previous engine that was being used. So that 510 // we can reuse the previous engine. 511 // 512 // Note that if TextToSpeech#getCurrentEngine is not null, it means at 513 // the very least that we successfully bound to the engine service. 514 mPreviousEngine = mTts.getCurrentEngine(); 515 516 // Step 1: Shut down the existing TTS engine. 517 if (mTts != null) { 518 try { 519 mTts.shutdown(); 520 mTts = null; 521 } catch (Exception e) { 522 Log.e(TAG, "Error shutting down TTS engine" + e); 523 } 524 } 525 526 // Step 2: Connect to the new TTS engine. 527 // Step 3 is continued on #onUpdateEngine (below) which is called when 528 // the app binds successfully to the engine. 529 if (DBG) Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine); 530 mTts = new TextToSpeech(getActivity().getApplicationContext(), mUpdateListener, engine); 531 setTtsUtteranceProgressListener(); 532 } 533 534 /* 535 * Step 3: We have now bound to the TTS engine the user requested. We will 536 * attempt to check voice data for the engine if we successfully bound to it, 537 * or revert to the previous engine if we didn't. 538 */ onUpdateEngine(int status)539 public void onUpdateEngine(int status) { 540 if (status == TextToSpeech.SUCCESS) { 541 if (DBG) { 542 Log.d(TAG, "Updating engine: Successfully bound to the engine: " + 543 mTts.getCurrentEngine()); 544 } 545 checkVoiceData(mTts.getCurrentEngine()); 546 } else { 547 if (DBG) Log.d(TAG, "Updating engine: Failed to bind to engine, reverting."); 548 if (mPreviousEngine != null) { 549 // This is guaranteed to at least bind, since mPreviousEngine would be 550 // null if the previous bind to this engine failed. 551 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener, 552 mPreviousEngine); 553 setTtsUtteranceProgressListener(); 554 } 555 mPreviousEngine = null; 556 } 557 } 558 559 /* 560 * Step 4: Check whether the voice data for the engine is ok. 561 */ checkVoiceData(String engine)562 private void checkVoiceData(String engine) { 563 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); 564 intent.setPackage(engine); 565 try { 566 if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0)); 567 startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); 568 } catch (ActivityNotFoundException ex) { 569 Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")"); 570 } 571 } 572 573 /* 574 * Step 5: The voice data check is complete. 575 */ onVoiceDataIntegrityCheckDone(Intent data)576 private void onVoiceDataIntegrityCheckDone(Intent data) { 577 final String engine = mTts.getCurrentEngine(); 578 579 if (engine == null) { 580 Log.e(TAG, "Voice data check complete, but no engine bound"); 581 return; 582 } 583 584 if (data == null){ 585 Log.e(TAG, "Engine failed voice data integrity check (null return)" + 586 mTts.getCurrentEngine()); 587 return; 588 } 589 590 android.provider.Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); 591 592 mAvailableStrLocals = data.getStringArrayListExtra( 593 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); 594 if (mAvailableStrLocals == null) { 595 Log.e(TAG, "Voice data check complete, but no available voices found"); 596 // Set mAvailableStrLocals to empty list 597 mAvailableStrLocals = new ArrayList<String>(); 598 } 599 if (evaluateDefaultLocale()) { 600 getSampleText(); 601 } 602 603 final int engineCount = mEnginePreferenceCategory.getPreferenceCount(); 604 for (int i = 0; i < engineCount; ++i) { 605 final Preference p = mEnginePreferenceCategory.getPreference(i); 606 if (p instanceof TtsEnginePreference) { 607 TtsEnginePreference enginePref = (TtsEnginePreference) p; 608 if (enginePref.getKey().equals(engine)) { 609 enginePref.setVoiceDataDetails(data); 610 break; 611 } 612 } 613 } 614 } 615 616 @Override getCurrentChecked()617 public Checkable getCurrentChecked() { 618 return mCurrentChecked; 619 } 620 621 @Override getCurrentKey()622 public String getCurrentKey() { 623 return mCurrentEngine; 624 } 625 626 @Override setCurrentChecked(Checkable current)627 public void setCurrentChecked(Checkable current) { 628 mCurrentChecked = current; 629 } 630 631 @Override setCurrentKey(String key)632 public void setCurrentKey(String key) { 633 mCurrentEngine = key; 634 updateDefaultEngine(mCurrentEngine); 635 } 636 637 } 638