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