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_PITCH; 20 import static android.provider.Settings.Secure.TTS_DEFAULT_RATE; 21 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; 22 23 import android.app.AlertDialog; 24 import android.content.ActivityNotFoundException; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.os.Bundle; 29 import android.provider.SearchIndexableResource; 30 import android.speech.tts.TextToSpeech; 31 import android.speech.tts.TextToSpeech.EngineInfo; 32 import android.speech.tts.TtsEngines; 33 import android.speech.tts.UtteranceProgressListener; 34 import android.support.v7.preference.ListPreference; 35 import android.support.v7.preference.Preference; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.util.Pair; 39 40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 41 import com.android.settings.R; 42 import com.android.settings.SettingsActivity; 43 import com.android.settings.SettingsPreferenceFragment; 44 import com.android.settings.search.BaseSearchIndexProvider; 45 import com.android.settings.search.Indexable; 46 import com.android.settings.widget.ActionButtonPreference; 47 import com.android.settings.widget.GearPreference; 48 import com.android.settings.widget.SeekBarPreference; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collections; 53 import java.util.Comparator; 54 import java.util.HashMap; 55 import java.util.List; 56 import java.util.Locale; 57 import java.util.MissingResourceException; 58 import java.util.Objects; 59 import java.util.Set; 60 61 public class TextToSpeechSettings extends SettingsPreferenceFragment 62 implements Preference.OnPreferenceChangeListener, 63 GearPreference.OnGearClickListener, Indexable { 64 65 private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries"; 66 private static final String STATE_KEY_LOCALE_ENTRY_VALUES = "locale_entry_values"; 67 private static final String STATE_KEY_LOCALE_VALUE = "locale_value"; 68 69 private static final String TAG = "TextToSpeechSettings"; 70 private static final boolean DBG = false; 71 72 /** Preference key for the TTS pitch selection slider. */ 73 private static final String KEY_DEFAULT_PITCH = "tts_default_pitch"; 74 75 /** Preference key for the TTS rate selection slider. */ 76 private static final String KEY_DEFAULT_RATE = "tts_default_rate"; 77 78 /** Engine picker. */ 79 private static final String KEY_TTS_ENGINE_PREFERENCE = "tts_engine_preference"; 80 81 /** Locale picker. */ 82 private static final String KEY_ENGINE_LOCALE = "tts_default_lang"; 83 84 /** Play/Reset buttons container. */ 85 private static final String KEY_ACTION_BUTTONS = "action_buttons"; 86 87 /** 88 * These look like birth years, but they aren't mine. I'm much younger than this. 89 */ 90 private static final int GET_SAMPLE_TEXT = 1983; 91 private static final int VOICE_DATA_INTEGRITY_CHECK = 1977; 92 93 /** 94 * Speech rate value. This value should be kept in sync with the max value set in tts_settings 95 * xml. 96 */ 97 private static final int MAX_SPEECH_RATE = 600; 98 99 private static final int MIN_SPEECH_RATE = 10; 100 101 /** 102 * Speech pitch value. TTS pitch value varies from 25 to 400, where 100 is the value for normal 103 * pitch. The max pitch value is set to 400, based on feedback from users and the GoogleTTS 104 * pitch variation range. The range for pitch is not set in stone and should be readjusted based 105 * on user need. 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 109 private static final int MIN_SPEECH_PITCH = 25; 110 111 private SeekBarPreference mDefaultPitchPref; 112 private SeekBarPreference mDefaultRatePref; 113 private ActionButtonPreference mActionButtons; 114 115 private int mDefaultPitch = TextToSpeech.Engine.DEFAULT_PITCH; 116 private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE; 117 118 private int mSelectedLocaleIndex = -1; 119 120 /** The currently selected engine. */ 121 private String mCurrentEngine; 122 123 private TextToSpeech mTts = null; 124 private TtsEngines mEnginesHelper = null; 125 126 private String mSampleText = null; 127 128 private ListPreference mLocalePreference; 129 130 /** 131 * Default locale used by selected TTS engine, null if not connected to any engine. 132 */ 133 private Locale mCurrentDefaultLocale; 134 135 /** 136 * List of available locals of selected TTS engine, as returned by 137 * {@link TextToSpeech.Engine#ACTION_CHECK_TTS_DATA} activity. If empty, then activity 138 * was not yet called. 139 */ 140 private List<String> mAvailableStrLocals; 141 142 /** 143 * The initialization listener used when we are initalizing the settings 144 * screen for the first time (as opposed to when a user changes his choice 145 * of engine). 146 */ 147 private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() { 148 @Override 149 public void onInit(int status) { 150 onInitEngine(status); 151 } 152 }; 153 154 @Override getMetricsCategory()155 public int getMetricsCategory() { 156 return MetricsEvent.TTS_TEXT_TO_SPEECH; 157 } 158 159 @Override onCreate(Bundle savedInstanceState)160 public void onCreate(Bundle savedInstanceState) { 161 super.onCreate(savedInstanceState); 162 addPreferencesFromResource(R.xml.tts_settings); 163 164 getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); 165 166 mEnginesHelper = new TtsEngines(getActivity().getApplicationContext()); 167 168 mLocalePreference = (ListPreference) findPreference(KEY_ENGINE_LOCALE); 169 mLocalePreference.setOnPreferenceChangeListener(this); 170 171 mDefaultPitchPref = (SeekBarPreference) findPreference(KEY_DEFAULT_PITCH); 172 mDefaultRatePref = (SeekBarPreference) findPreference(KEY_DEFAULT_RATE); 173 174 mActionButtons = ((ActionButtonPreference) findPreference(KEY_ACTION_BUTTONS)) 175 .setButton1Text(R.string.tts_play) 176 .setButton1Positive(true) 177 .setButton1OnClickListener(v -> speakSampleText()) 178 .setButton1Enabled(false) 179 .setButton2Text(R.string.tts_reset) 180 .setButton2Positive(false) 181 .setButton2OnClickListener(v -> resetTts()) 182 .setButton1Enabled(true); 183 184 if (savedInstanceState == null) { 185 mLocalePreference.setEnabled(false); 186 mLocalePreference.setEntries(new CharSequence[0]); 187 mLocalePreference.setEntryValues(new CharSequence[0]); 188 } else { 189 // Repopulate mLocalePreference with saved state. Will be updated later with 190 // up-to-date values when checkTtsData() calls back with results. 191 final CharSequence[] entries = 192 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRIES); 193 final CharSequence[] entryValues = 194 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES); 195 final CharSequence value = savedInstanceState.getCharSequence(STATE_KEY_LOCALE_VALUE); 196 197 mLocalePreference.setEntries(entries); 198 mLocalePreference.setEntryValues(entryValues); 199 mLocalePreference.setValue(value != null ? value.toString() : null); 200 mLocalePreference.setEnabled(entries.length > 0); 201 } 202 203 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener); 204 205 setTtsUtteranceProgressListener(); 206 initSettings(); 207 208 // Prevent restarting the TTS connection on rotation 209 setRetainInstance(true); 210 } 211 212 @Override onResume()213 public void onResume() { 214 super.onResume(); 215 216 if (mTts == null || mCurrentDefaultLocale == null) { 217 return; 218 } 219 if (!mTts.getDefaultEngine().equals(mTts.getCurrentEngine())) { 220 try { 221 mTts.shutdown(); 222 mTts = null; 223 } catch (Exception e) { 224 Log.e(TAG, "Error shutting down TTS engine" + e); 225 } 226 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener); 227 setTtsUtteranceProgressListener(); 228 initSettings(); 229 } else { 230 // Do set pitch correctly after it may have changed, and unlike speed, it doesn't change 231 // immediately. 232 final ContentResolver resolver = getContentResolver(); 233 mTts.setPitch(android.provider.Settings.Secure.getInt(resolver, TTS_DEFAULT_PITCH, 234 TextToSpeech.Engine.DEFAULT_PITCH) / 100.0f); 235 } 236 237 Locale ttsDefaultLocale = mTts.getDefaultLanguage(); 238 if (mCurrentDefaultLocale != null && !mCurrentDefaultLocale.equals(ttsDefaultLocale)) { 239 updateWidgetState(false); 240 checkDefaultLocale(); 241 } 242 } 243 setTtsUtteranceProgressListener()244 private void setTtsUtteranceProgressListener() { 245 if (mTts == null) { 246 return; 247 } 248 mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() { 249 @Override 250 public void onStart(String utteranceId) { 251 } 252 253 @Override 254 public void onDone(String utteranceId) { 255 } 256 257 @Override 258 public void onError(String utteranceId) { 259 Log.e(TAG, "Error while trying to synthesize sample text"); 260 } 261 }); 262 } 263 264 @Override onDestroy()265 public void onDestroy() { 266 super.onDestroy(); 267 if (mTts != null) { 268 mTts.shutdown(); 269 mTts = null; 270 } 271 } 272 273 @Override onSaveInstanceState(Bundle outState)274 public void onSaveInstanceState(Bundle outState) { 275 super.onSaveInstanceState(outState); 276 277 // Save the mLocalePreference values, so we can repopulate it with entries. 278 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRIES, 279 mLocalePreference.getEntries()); 280 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES, 281 mLocalePreference.getEntryValues()); 282 outState.putCharSequence(STATE_KEY_LOCALE_VALUE, 283 mLocalePreference.getValue()); 284 } 285 initSettings()286 private void initSettings() { 287 final ContentResolver resolver = getContentResolver(); 288 289 // Set up the default rate and pitch. 290 mDefaultRate = 291 android.provider.Settings.Secure.getInt( 292 resolver, TTS_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE); 293 mDefaultPitch = 294 android.provider.Settings.Secure.getInt( 295 resolver, TTS_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH); 296 297 mDefaultRatePref.setProgress(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, mDefaultRate)); 298 mDefaultRatePref.setOnPreferenceChangeListener(this); 299 mDefaultRatePref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, MAX_SPEECH_RATE)); 300 301 mDefaultPitchPref.setProgress( 302 getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, mDefaultPitch)); 303 mDefaultPitchPref.setOnPreferenceChangeListener(this); 304 mDefaultPitchPref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, MAX_SPEECH_PITCH)); 305 306 if (mTts != null) { 307 mCurrentEngine = mTts.getCurrentEngine(); 308 mTts.setSpeechRate(mDefaultRate / 100.0f); 309 mTts.setPitch(mDefaultPitch / 100.0f); 310 } 311 312 SettingsActivity activity = null; 313 if (getActivity() instanceof SettingsActivity) { 314 activity = (SettingsActivity) getActivity(); 315 } else { 316 throw new IllegalStateException("TextToSpeechSettings used outside a " + 317 "Settings"); 318 } 319 320 if (mCurrentEngine != null) { 321 EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine); 322 323 324 Preference mEnginePreference = findPreference(KEY_TTS_ENGINE_PREFERENCE); 325 ((GearPreference) mEnginePreference).setOnGearClickListener(this); 326 mEnginePreference.setSummary(info.label); 327 } 328 329 checkVoiceData(mCurrentEngine); 330 } 331 332 /** 333 * The minimum speech pitch/rate value should be > 0 but the minimum value of a seekbar in 334 * android is fixed at 0. Therefore, we increment the seekbar progress with MIN_SPEECH_VALUE so 335 * that the minimum seekbar progress value is MIN_SPEECH_PITCH/RATE. SPEECH_VALUE = 336 * MIN_SPEECH_VALUE + SEEKBAR_PROGRESS 337 */ getValueFromSeekBarProgress(String preferenceKey, int progress)338 private int getValueFromSeekBarProgress(String preferenceKey, int progress) { 339 if (preferenceKey.equals(KEY_DEFAULT_RATE)) { 340 return MIN_SPEECH_RATE + progress; 341 } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) { 342 return MIN_SPEECH_PITCH + progress; 343 } 344 return progress; 345 } 346 347 /** 348 * Since we are appending the MIN_SPEECH value to the speech seekbar progress, the speech 349 * seekbar progress should be set to (speechValue - MIN_SPEECH value). 350 */ getSeekBarProgressFromValue(String preferenceKey, int value)351 private int getSeekBarProgressFromValue(String preferenceKey, int value) { 352 if (preferenceKey.equals(KEY_DEFAULT_RATE)) { 353 return value - MIN_SPEECH_RATE; 354 } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) { 355 return value - MIN_SPEECH_PITCH; 356 } 357 return value; 358 } 359 360 /** Called when the TTS engine is initialized. */ onInitEngine(int status)361 public void onInitEngine(int status) { 362 if (status == TextToSpeech.SUCCESS) { 363 if (DBG) Log.d(TAG, "TTS engine for settings screen initialized."); 364 checkDefaultLocale(); 365 getActivity() 366 .runOnUiThread( 367 new Runnable() { 368 @Override 369 public void run() { 370 mLocalePreference.setEnabled(true); 371 } 372 }); 373 } else { 374 if (DBG) { 375 Log.d(TAG, 376 "TTS engine for settings screen failed to initialize successfully."); 377 } 378 updateWidgetState(false); 379 } 380 } 381 checkDefaultLocale()382 private void checkDefaultLocale() { 383 Locale defaultLocale = mTts.getDefaultLanguage(); 384 if (defaultLocale == null) { 385 Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine); 386 updateWidgetState(false); 387 return; 388 } 389 390 // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize, 391 // we may end up with English (USA)and German (DEU). 392 final Locale oldDefaultLocale = mCurrentDefaultLocale; 393 mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString()); 394 if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) { 395 mSampleText = null; 396 } 397 398 int defaultAvailable = mTts.setLanguage(defaultLocale); 399 if (evaluateDefaultLocale() && mSampleText == null) { 400 getSampleText(); 401 } 402 } 403 evaluateDefaultLocale()404 private boolean evaluateDefaultLocale() { 405 // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list 406 // of available languages. 407 if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) { 408 return false; 409 } 410 411 boolean notInAvailableLangauges = true; 412 try { 413 // Check if language is listed in CheckVoices Action result as available voice. 414 String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language(); 415 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) { 416 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country(); 417 } 418 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) { 419 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant(); 420 } 421 422 for (String loc : mAvailableStrLocals) { 423 if (loc.equalsIgnoreCase(defaultLocaleStr)) { 424 notInAvailableLangauges = false; 425 break; 426 } 427 } 428 } catch (MissingResourceException e) { 429 if (DBG) Log.wtf(TAG, "MissingResourceException", e); 430 updateWidgetState(false); 431 return false; 432 } 433 434 int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale); 435 if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED || 436 defaultAvailable == TextToSpeech.LANG_MISSING_DATA || 437 notInAvailableLangauges) { 438 if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported."); 439 updateWidgetState(false); 440 return false; 441 } else { 442 updateWidgetState(true); 443 return true; 444 } 445 } 446 447 /** 448 * Ask the current default engine to return a string of sample text to be 449 * spoken to the user. 450 */ getSampleText()451 private void getSampleText() { 452 String currentEngine = mTts.getCurrentEngine(); 453 454 if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine(); 455 456 // TODO: This is currently a hidden private API. The intent extras 457 // and the intent action should be made public if we intend to make this 458 // a public API. We fall back to using a canned set of strings if this 459 // doesn't work. 460 Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); 461 462 intent.putExtra("language", mCurrentDefaultLocale.getLanguage()); 463 intent.putExtra("country", mCurrentDefaultLocale.getCountry()); 464 intent.putExtra("variant", mCurrentDefaultLocale.getVariant()); 465 intent.setPackage(currentEngine); 466 467 try { 468 if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0)); 469 startActivityForResult(intent, GET_SAMPLE_TEXT); 470 } catch (ActivityNotFoundException ex) { 471 Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")"); 472 } 473 } 474 475 /** 476 * Called when voice data integrity check returns 477 */ 478 @Override onActivityResult(int requestCode, int resultCode, Intent data)479 public void onActivityResult(int requestCode, int resultCode, Intent data) { 480 if (requestCode == GET_SAMPLE_TEXT) { 481 onSampleTextReceived(resultCode, data); 482 } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { 483 onVoiceDataIntegrityCheckDone(data); 484 if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) { 485 updateDefaultLocalePref(data); 486 } 487 } 488 } 489 updateDefaultLocalePref(Intent data)490 private void updateDefaultLocalePref(Intent data) { 491 final ArrayList<String> availableLangs = 492 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); 493 494 final ArrayList<String> unavailableLangs = 495 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES); 496 497 if (availableLangs == null || availableLangs.size() == 0) { 498 mLocalePreference.setEnabled(false); 499 return; 500 } 501 Locale currentLocale = null; 502 if (!mEnginesHelper.isLocaleSetToDefaultForEngine(mTts.getCurrentEngine())) { 503 currentLocale = mEnginesHelper.getLocalePrefForEngine(mTts.getCurrentEngine()); 504 } 505 506 ArrayList<Pair<String, Locale>> entryPairs = 507 new ArrayList<Pair<String, Locale>>(availableLangs.size()); 508 for (int i = 0; i < availableLangs.size(); i++) { 509 Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i)); 510 if (locale != null) { 511 entryPairs.add(new Pair<String, Locale>(locale.getDisplayName(), locale)); 512 } 513 } 514 515 // Sort it 516 Collections.sort( 517 entryPairs, 518 new Comparator<Pair<String, Locale>>() { 519 @Override 520 public int compare(Pair<String, Locale> lhs, Pair<String, Locale> rhs) { 521 return lhs.first.compareToIgnoreCase(rhs.first); 522 } 523 }); 524 525 // Get two arrays out of one of pairs 526 mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value 527 CharSequence[] entries = new CharSequence[availableLangs.size() + 1]; 528 CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1]; 529 530 entries[0] = getActivity().getString(R.string.tts_lang_use_system); 531 entryValues[0] = ""; 532 533 int i = 1; 534 for (Pair<String, Locale> entry : entryPairs) { 535 if (entry.second.equals(currentLocale)) { 536 mSelectedLocaleIndex = i; 537 } 538 entries[i] = entry.first; 539 entryValues[i++] = entry.second.toString(); 540 } 541 542 mLocalePreference.setEntries(entries); 543 mLocalePreference.setEntryValues(entryValues); 544 mLocalePreference.setEnabled(true); 545 setLocalePreference(mSelectedLocaleIndex); 546 } 547 548 /** Set entry from entry table in mLocalePreference */ setLocalePreference(int index)549 private void setLocalePreference(int index) { 550 if (index < 0) { 551 mLocalePreference.setValue(""); 552 mLocalePreference.setSummary(R.string.tts_lang_not_selected); 553 } else { 554 mLocalePreference.setValueIndex(index); 555 mLocalePreference.setSummary(mLocalePreference.getEntries()[index]); 556 } 557 } 558 559 getDefaultSampleString()560 private String getDefaultSampleString() { 561 if (mTts != null && mTts.getLanguage() != null) { 562 try { 563 final String currentLang = mTts.getLanguage().getISO3Language(); 564 String[] strings = getActivity().getResources().getStringArray( 565 R.array.tts_demo_strings); 566 String[] langs = getActivity().getResources().getStringArray( 567 R.array.tts_demo_string_langs); 568 569 for (int i = 0; i < strings.length; ++i) { 570 if (langs[i].equals(currentLang)) { 571 return strings[i]; 572 } 573 } 574 } catch (MissingResourceException e) { 575 if (DBG) Log.wtf(TAG, "MissingResourceException", e); 576 // Ignore and fall back to default sample string 577 } 578 } 579 return getString(R.string.tts_default_sample_string); 580 } 581 isNetworkRequiredForSynthesis()582 private boolean isNetworkRequiredForSynthesis() { 583 Set<String> features = mTts.getFeatures(mCurrentDefaultLocale); 584 if (features == null) { 585 return false; 586 } 587 return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) && 588 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS); 589 } 590 onSampleTextReceived(int resultCode, Intent data)591 private void onSampleTextReceived(int resultCode, Intent data) { 592 String sample = getDefaultSampleString(); 593 594 if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) { 595 if (data != null && data.getStringExtra("sampleText") != null) { 596 sample = data.getStringExtra("sampleText"); 597 } 598 if (DBG) Log.d(TAG, "Got sample text: " + sample); 599 } else { 600 if (DBG) Log.d(TAG, "Using default sample text :" + sample); 601 } 602 603 mSampleText = sample; 604 if (mSampleText != null) { 605 updateWidgetState(true); 606 } else { 607 Log.e(TAG, "Did not have a sample string for the requested language. Using default"); 608 } 609 } 610 speakSampleText()611 private void speakSampleText() { 612 final boolean networkRequired = isNetworkRequiredForSynthesis(); 613 if (!networkRequired || networkRequired && 614 (mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE)) { 615 HashMap<String, String> params = new HashMap<String, String>(); 616 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample"); 617 618 mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params); 619 } else { 620 Log.w(TAG, "Network required for sample synthesis for requested language"); 621 displayNetworkAlert(); 622 } 623 } 624 625 @Override onPreferenceChange(Preference preference, Object objValue)626 public boolean onPreferenceChange(Preference preference, Object objValue) { 627 if (KEY_DEFAULT_RATE.equals(preference.getKey())) { 628 updateSpeechRate((Integer) objValue); 629 } else if (KEY_DEFAULT_PITCH.equals(preference.getKey())) { 630 updateSpeechPitchValue((Integer) objValue); 631 } else if (preference == mLocalePreference) { 632 String localeString = (String) objValue; 633 updateLanguageTo( 634 (!TextUtils.isEmpty(localeString) 635 ? mEnginesHelper.parseLocaleString(localeString) 636 : null)); 637 checkDefaultLocale(); 638 return true; 639 } 640 return true; 641 } 642 updateLanguageTo(Locale locale)643 private void updateLanguageTo(Locale locale) { 644 int selectedLocaleIndex = -1; 645 String localeString = (locale != null) ? locale.toString() : ""; 646 for (int i = 0; i < mLocalePreference.getEntryValues().length; i++) { 647 if (localeString.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) { 648 selectedLocaleIndex = i; 649 break; 650 } 651 } 652 653 if (selectedLocaleIndex == -1) { 654 Log.w(TAG, "updateLanguageTo called with unknown locale argument"); 655 return; 656 } 657 mLocalePreference.setSummary(mLocalePreference.getEntries()[selectedLocaleIndex]); 658 mSelectedLocaleIndex = selectedLocaleIndex; 659 660 mEnginesHelper.updateLocalePrefForEngine(mTts.getCurrentEngine(), locale); 661 662 // Null locale means "use system default" 663 mTts.setLanguage((locale != null) ? locale : Locale.getDefault()); 664 } 665 resetTts()666 private void resetTts() { 667 // Reset button. 668 int speechRateSeekbarProgress = 669 getSeekBarProgressFromValue( 670 KEY_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE); 671 mDefaultRatePref.setProgress(speechRateSeekbarProgress); 672 updateSpeechRate(speechRateSeekbarProgress); 673 int pitchSeekbarProgress = 674 getSeekBarProgressFromValue( 675 KEY_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH); 676 mDefaultPitchPref.setProgress(pitchSeekbarProgress); 677 updateSpeechPitchValue(pitchSeekbarProgress); 678 } 679 updateSpeechRate(int speechRateSeekBarProgress)680 private void updateSpeechRate(int speechRateSeekBarProgress) { 681 mDefaultRate = getValueFromSeekBarProgress(KEY_DEFAULT_RATE, speechRateSeekBarProgress); 682 try { 683 android.provider.Settings.Secure.putInt( 684 getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate); 685 if (mTts != null) { 686 mTts.setSpeechRate(mDefaultRate / 100.0f); 687 } 688 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate); 689 } catch (NumberFormatException e) { 690 Log.e(TAG, "could not persist default TTS rate setting", e); 691 } 692 return; 693 } 694 updateSpeechPitchValue(int speechPitchSeekBarProgress)695 private void updateSpeechPitchValue(int speechPitchSeekBarProgress) { 696 mDefaultPitch = getValueFromSeekBarProgress(KEY_DEFAULT_PITCH, speechPitchSeekBarProgress); 697 try { 698 android.provider.Settings.Secure.putInt( 699 getContentResolver(), TTS_DEFAULT_PITCH, mDefaultPitch); 700 if (mTts != null) { 701 mTts.setPitch(mDefaultPitch / 100.0f); 702 } 703 if (DBG) Log.d(TAG, "TTS default pitch changed, now" + mDefaultPitch); 704 } catch (NumberFormatException e) { 705 Log.e(TAG, "could not persist default TTS pitch setting", e); 706 } 707 return; 708 } 709 updateWidgetState(boolean enable)710 private void updateWidgetState(boolean enable) { 711 mActionButtons.setButton1Enabled(enable); 712 mDefaultRatePref.setEnabled(enable); 713 mDefaultPitchPref.setEnabled(enable); 714 } 715 displayNetworkAlert()716 private void displayNetworkAlert() { 717 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 718 builder.setTitle(android.R.string.dialog_alert_title) 719 .setMessage(getActivity().getString(R.string.tts_engine_network_required)) 720 .setCancelable(false) 721 .setPositiveButton(android.R.string.ok, null); 722 723 AlertDialog dialog = builder.create(); 724 dialog.show(); 725 } 726 727 /** Check whether the voice data for the engine is ok. */ checkVoiceData(String engine)728 private void checkVoiceData(String engine) { 729 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); 730 intent.setPackage(engine); 731 try { 732 if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0)); 733 startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); 734 } catch (ActivityNotFoundException ex) { 735 Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")"); 736 } 737 } 738 739 /** The voice data check is complete. */ onVoiceDataIntegrityCheckDone(Intent data)740 private void onVoiceDataIntegrityCheckDone(Intent data) { 741 final String engine = mTts.getCurrentEngine(); 742 743 if (engine == null) { 744 Log.e(TAG, "Voice data check complete, but no engine bound"); 745 return; 746 } 747 748 if (data == null) { 749 Log.e(TAG, "Engine failed voice data integrity check (null return)" + 750 mTts.getCurrentEngine()); 751 return; 752 } 753 754 android.provider.Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); 755 756 mAvailableStrLocals = data.getStringArrayListExtra( 757 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); 758 if (mAvailableStrLocals == null) { 759 Log.e(TAG, "Voice data check complete, but no available voices found"); 760 // Set mAvailableStrLocals to empty list 761 mAvailableStrLocals = new ArrayList<String>(); 762 } 763 if (evaluateDefaultLocale()) { 764 getSampleText(); 765 } 766 } 767 768 @Override onGearClick(GearPreference p)769 public void onGearClick(GearPreference p) { 770 if (KEY_TTS_ENGINE_PREFERENCE.equals(p.getKey())) { 771 EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine); 772 final Intent settingsIntent = mEnginesHelper.getSettingsIntent(info.name); 773 if (settingsIntent != null) { 774 startActivity(settingsIntent); 775 } else { 776 Log.e(TAG, "settingsIntent is null"); 777 } 778 } 779 } 780 781 public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 782 new BaseSearchIndexProvider() { 783 @Override 784 public List<SearchIndexableResource> getXmlResourcesToIndex( 785 Context context, boolean enabled) { 786 final SearchIndexableResource sir = new SearchIndexableResource(context); 787 sir.xmlResId = R.xml.tts_settings; 788 return Arrays.asList(sir); 789 } 790 791 @Override 792 public List<String> getNonIndexableKeys(Context context) { 793 final List<String> keys = super.getNonIndexableKeys(context); 794 keys.add("tts_engine_preference"); 795 return keys; 796 } 797 }; 798 799 } 800