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