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