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.settings.tts; 18 19 import android.car.drivingstate.CarUxRestrictions; 20 import android.content.ActivityNotFoundException; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.provider.Settings; 24 import android.speech.tts.TextToSpeech; 25 import android.speech.tts.TtsEngines; 26 import android.text.TextUtils; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.annotation.VisibleForTesting; 31 import androidx.preference.ListPreference; 32 import androidx.preference.Preference; 33 import androidx.preference.PreferenceGroup; 34 35 import com.android.car.settings.R; 36 import com.android.car.settings.common.ActivityResultCallback; 37 import com.android.car.settings.common.FragmentController; 38 import com.android.car.settings.common.Logger; 39 import com.android.car.settings.common.PreferenceController; 40 import com.android.car.settings.common.SeekBarPreference; 41 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.Locale; 45 import java.util.Objects; 46 47 /** 48 * Business logic for configuring and listening to the current TTS voice. This preference contorller 49 * handles the following: 50 * 51 * <ol> 52 * <li>Changing the TTS language 53 * <li>Changing the TTS speech rate 54 * <li>Changing the TTS voice pitch 55 * <li>Resetting the TTS configuration 56 * </ol> 57 */ 58 public class TtsPlaybackPreferenceController extends 59 PreferenceController<PreferenceGroup> implements ActivityResultCallback { 60 61 private static final Logger LOG = new Logger(TtsPlaybackPreferenceController.class); 62 63 @VisibleForTesting 64 static final int VOICE_DATA_CHECK = 1; 65 @VisibleForTesting 66 static final int GET_SAMPLE_TEXT = 2; 67 68 private final TtsEngines mEnginesHelper; 69 private TtsPlaybackSettingsManager mTtsPlaybackManager; 70 private TextToSpeech mTts; 71 private int mSelectedLocaleIndex; 72 73 private ListPreference mDefaultLanguagePreference; 74 private SeekBarPreference mSpeechRatePreference; 75 private SeekBarPreference mVoicePitchPreference; 76 private Preference mResetPreference; 77 78 private String mSampleText; 79 private Locale mSampleTextLocale; 80 81 /** True if initialized with no errors. */ 82 private boolean mTtsInitialized = false; 83 84 private final TextToSpeech.OnInitListener mOnInitListener = status -> { 85 if (status == TextToSpeech.SUCCESS) { 86 mTtsInitialized = true; 87 refreshUi(); 88 } 89 }; 90 TtsPlaybackPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)91 public TtsPlaybackPreferenceController(Context context, String preferenceKey, 92 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 93 super(context, preferenceKey, fragmentController, uxRestrictions); 94 mEnginesHelper = new TtsEngines(context); 95 } 96 97 @Override getPreferenceType()98 protected Class<PreferenceGroup> getPreferenceType() { 99 return PreferenceGroup.class; 100 } 101 102 @Override onCreateInternal()103 protected void onCreateInternal() { 104 mDefaultLanguagePreference = initDefaultLanguagePreference(); 105 mSpeechRatePreference = initSpeechRatePreference(); 106 mVoicePitchPreference = initVoicePitchPreference(); 107 mResetPreference = initResetTtsPlaybackPreference(); 108 109 mTts = new TextToSpeech(getContext(), mOnInitListener); 110 mTtsPlaybackManager = new TtsPlaybackSettingsManager(getContext(), mTts, mEnginesHelper); 111 mTts.setSpeechRate(mTtsPlaybackManager.getCurrentSpeechRate() 112 / TtsPlaybackSettingsManager.SCALING_FACTOR); 113 mTts.setPitch(mTtsPlaybackManager.getCurrentVoicePitch() 114 / TtsPlaybackSettingsManager.SCALING_FACTOR); 115 startEngineVoiceDataCheck(mTts.getCurrentEngine()); 116 } 117 118 @Override onDestroyInternal()119 protected void onDestroyInternal() { 120 if (mTts != null) { 121 mTts.shutdown(); 122 mTts = null; 123 mTtsPlaybackManager = null; 124 } 125 } 126 127 @Override updateState(PreferenceGroup preference)128 protected void updateState(PreferenceGroup preference) { 129 boolean isValid = isDefaultLocaleValid(); 130 mDefaultLanguagePreference.setEnabled(isValid); 131 mSpeechRatePreference.setEnabled(isValid); 132 mVoicePitchPreference.setEnabled(isValid); 133 mResetPreference.setEnabled(isValid); 134 if (!isValid && mDefaultLanguagePreference.getEntries() != null) { 135 mDefaultLanguagePreference.setEnabled(true); 136 } 137 138 if (mDefaultLanguagePreference.getEntries() != null) { 139 mDefaultLanguagePreference.setValueIndex(mSelectedLocaleIndex); 140 mDefaultLanguagePreference.setSummary( 141 mDefaultLanguagePreference.getEntries()[mSelectedLocaleIndex]); 142 } 143 144 mSpeechRatePreference.setValue(mTtsPlaybackManager.getCurrentSpeechRate()); 145 mVoicePitchPreference.setValue(mTtsPlaybackManager.getCurrentVoicePitch()); 146 checkOrUpdateSampleText(); 147 } 148 149 @Override processActivityResult(int requestCode, int resultCode, @Nullable Intent data)150 public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 151 switch (requestCode) { 152 case VOICE_DATA_CHECK: 153 onVoiceDataIntegrityCheckDone(resultCode, data); 154 break; 155 case GET_SAMPLE_TEXT: 156 onSampleTextReceived(resultCode, data); 157 break; 158 default: 159 LOG.e("Got unknown activity result"); 160 } 161 } 162 startEngineVoiceDataCheck(String engine)163 private void startEngineVoiceDataCheck(String engine) { 164 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); 165 intent.setPackage(engine); 166 try { 167 LOG.d("Updating engine: Checking voice data: " + intent.toUri(0)); 168 getFragmentController().startActivityForResult(intent, VOICE_DATA_CHECK, 169 this); 170 } catch (ActivityNotFoundException ex) { 171 LOG.e("Failed to check TTS data, no activity found for " + intent); 172 } 173 } 174 175 /** 176 * Ask the current default engine to return a string of sample text to be 177 * spoken to the user. 178 */ startGetSampleText()179 private void startGetSampleText() { 180 String currentEngine = mTts.getCurrentEngine(); 181 if (TextUtils.isEmpty(currentEngine)) { 182 currentEngine = mTts.getDefaultEngine(); 183 } 184 185 Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); 186 mSampleTextLocale = mTtsPlaybackManager.getEffectiveTtsLocale(); 187 if (mSampleTextLocale == null) { 188 return; 189 } 190 intent.putExtra(TextToSpeech.Engine.KEY_PARAM_LANGUAGE, mSampleTextLocale.getLanguage()); 191 intent.putExtra(TextToSpeech.Engine.KEY_PARAM_COUNTRY, mSampleTextLocale.getCountry()); 192 intent.putExtra(TextToSpeech.Engine.KEY_PARAM_VARIANT, mSampleTextLocale.getVariant()); 193 intent.setPackage(currentEngine); 194 195 try { 196 LOG.d("Getting sample text: " + intent.toUri(0)); 197 getFragmentController().startActivityForResult(intent, GET_SAMPLE_TEXT, this); 198 } catch (ActivityNotFoundException ex) { 199 LOG.e("Failed to get sample text, no activity found for " + intent + ")"); 200 } 201 } 202 203 /** The voice data check is complete. */ onVoiceDataIntegrityCheckDone(int resultCode, Intent data)204 private void onVoiceDataIntegrityCheckDone(int resultCode, Intent data) { 205 String engine = mTts.getCurrentEngine(); 206 if (engine == null) { 207 LOG.e("Voice data check complete, but no engine bound"); 208 return; 209 } 210 211 if (data == null || resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) { 212 LOG.e("Engine failed voice data integrity check (null return or invalid result code)" 213 + mTts.getCurrentEngine()); 214 return; 215 } 216 217 Settings.Secure.putString(getContext().getContentResolver(), 218 Settings.Secure.TTS_DEFAULT_SYNTH, engine); 219 220 ArrayList<String> availableLangs = 221 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); 222 if (availableLangs == null || availableLangs.size() == 0) { 223 refreshUi(); 224 return; 225 } 226 227 updateDefaultLanguagePreference(availableLangs); 228 229 mSelectedLocaleIndex = findLocaleIndex(mTtsPlaybackManager.getStoredTtsLocale()); 230 if (mSelectedLocaleIndex < 0) { 231 mSelectedLocaleIndex = 0; 232 } 233 startGetSampleText(); 234 refreshUi(); 235 } 236 onSampleTextReceived(int resultCode, Intent data)237 private void onSampleTextReceived(int resultCode, Intent data) { 238 String sample = getContext().getString(R.string.tts_default_sample_string); 239 240 if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) { 241 String tmp = data.getStringExtra(TextToSpeech.Engine.EXTRA_SAMPLE_TEXT); 242 if (!TextUtils.isEmpty(tmp)) { 243 sample = tmp; 244 } 245 LOG.d("Got sample text: " + sample); 246 } else { 247 LOG.d("Using default sample text :" + sample); 248 } 249 250 mSampleText = sample; 251 } 252 updateLanguageTo(Locale locale)253 private void updateLanguageTo(Locale locale) { 254 int selectedLocaleIndex = findLocaleIndex(locale); 255 if (selectedLocaleIndex == -1) { 256 LOG.w("updateLanguageTo called with unknown locale argument"); 257 return; 258 } 259 260 if (mTtsPlaybackManager.updateTtsLocale(locale)) { 261 mSelectedLocaleIndex = selectedLocaleIndex; 262 refreshUi(); 263 } else { 264 LOG.e("updateLanguageTo failed to update tts language"); 265 } 266 } 267 findLocaleIndex(Locale locale)268 private int findLocaleIndex(Locale locale) { 269 String localeString = (locale != null) ? locale.toString() : ""; 270 return mDefaultLanguagePreference.findIndexOfValue(localeString); 271 } 272 isDefaultLocaleValid()273 private boolean isDefaultLocaleValid() { 274 if (!mTtsInitialized) { 275 return false; 276 } 277 278 Locale defaultLocale = mTtsPlaybackManager.getEffectiveTtsLocale(); 279 if (defaultLocale == null) { 280 LOG.e("Failed to get default language from engine " + mTts.getCurrentEngine()); 281 return false; 282 } 283 284 if (mDefaultLanguagePreference.getEntries() == null) { 285 return false; 286 } 287 288 int index = mDefaultLanguagePreference.findIndexOfValue(defaultLocale.toString()); 289 if (index < 0) { 290 return false; 291 } 292 293 return true; 294 } 295 checkOrUpdateSampleText()296 private void checkOrUpdateSampleText() { 297 if (!mTtsInitialized) { 298 return; 299 } 300 Locale defaultLocale = mTtsPlaybackManager.getEffectiveTtsLocale(); 301 if (defaultLocale == null) { 302 LOG.e("Failed to get default language from engine " + mTts.getCurrentEngine()); 303 return; 304 } 305 306 if (!Objects.equals(defaultLocale, mSampleTextLocale)) { 307 mSampleText = null; 308 mSampleTextLocale = null; 309 } 310 311 if (mSampleText == null) { 312 startGetSampleText(); 313 } 314 } 315 316 @VisibleForTesting getSampleText()317 String getSampleText() { 318 return mSampleText; 319 } 320 321 /* ***************************************************************************************** * 322 * Preference initialization/update code. * 323 * ***************************************************************************************** */ 324 initDefaultLanguagePreference()325 private ListPreference initDefaultLanguagePreference() { 326 ListPreference defaultLanguagePreference = (ListPreference) getPreference().findPreference( 327 getContext().getString(R.string.pk_tts_default_language)); 328 defaultLanguagePreference.setOnPreferenceChangeListener((preference, newValue) -> { 329 String localeString = (String) newValue; 330 updateLanguageTo(!TextUtils.isEmpty(localeString) ? mEnginesHelper.parseLocaleString( 331 localeString) : null); 332 checkOrUpdateSampleText(); 333 return true; 334 }); 335 return defaultLanguagePreference; 336 } 337 updateDefaultLanguagePreference(@onNull ArrayList<String> availableLangs)338 private void updateDefaultLanguagePreference(@NonNull ArrayList<String> availableLangs) { 339 // Sort locales by display name. 340 ArrayList<Locale> locales = new ArrayList<>(); 341 for (int i = 0; i < availableLangs.size(); i++) { 342 Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i)); 343 if (locale != null) { 344 locales.add(locale); 345 } 346 } 347 Collections.sort(locales, 348 (lhs, rhs) -> lhs.getDisplayName().compareToIgnoreCase(rhs.getDisplayName())); 349 350 // Separate pairs into two separate arrays. 351 CharSequence[] entries = new CharSequence[availableLangs.size() + 1]; 352 CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1]; 353 354 entries[0] = getContext().getString(R.string.tts_lang_use_system); 355 entryValues[0] = ""; 356 357 int i = 1; 358 for (Locale locale : locales) { 359 entries[i] = locale.getDisplayName(); 360 entryValues[i++] = locale.toString(); 361 } 362 363 mDefaultLanguagePreference.setEntries(entries); 364 mDefaultLanguagePreference.setEntryValues(entryValues); 365 } 366 initSpeechRatePreference()367 private SeekBarPreference initSpeechRatePreference() { 368 SeekBarPreference speechRatePreference = (SeekBarPreference) getPreference().findPreference( 369 getContext().getString(R.string.pk_tts_speech_rate)); 370 speechRatePreference.setMin(TtsPlaybackSettingsManager.MIN_SPEECH_RATE); 371 speechRatePreference.setMax(TtsPlaybackSettingsManager.MAX_SPEECH_RATE); 372 speechRatePreference.setShowSeekBarValue(false); 373 speechRatePreference.setContinuousUpdate(false); 374 speechRatePreference.setOnPreferenceChangeListener((preference, newValue) -> { 375 if (mTtsPlaybackManager != null) { 376 mTtsPlaybackManager.updateSpeechRate((Integer) newValue); 377 mTtsPlaybackManager.speakSampleText(mSampleText); 378 return true; 379 } 380 LOG.e("speech rate preference enabled before it is allowed"); 381 return false; 382 }); 383 384 // Initially disable. 385 speechRatePreference.setEnabled(false); 386 return speechRatePreference; 387 } 388 initVoicePitchPreference()389 private SeekBarPreference initVoicePitchPreference() { 390 SeekBarPreference pitchPreference = (SeekBarPreference) getPreference().findPreference( 391 getContext().getString(R.string.pk_tts_pitch)); 392 pitchPreference.setMin(TtsPlaybackSettingsManager.MIN_VOICE_PITCH); 393 pitchPreference.setMax(TtsPlaybackSettingsManager.MAX_VOICE_PITCH); 394 pitchPreference.setShowSeekBarValue(false); 395 pitchPreference.setContinuousUpdate(false); 396 pitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { 397 if (mTtsPlaybackManager != null) { 398 mTtsPlaybackManager.updateVoicePitch((Integer) newValue); 399 mTtsPlaybackManager.speakSampleText(mSampleText); 400 return true; 401 } 402 LOG.e("speech pitch preference enabled before it is allowed"); 403 return false; 404 }); 405 406 // Initially disable. 407 pitchPreference.setEnabled(false); 408 return pitchPreference; 409 } 410 initResetTtsPlaybackPreference()411 private Preference initResetTtsPlaybackPreference() { 412 Preference resetPreference = getPreference().findPreference( 413 getContext().getString(R.string.pk_tts_reset)); 414 resetPreference.setOnPreferenceClickListener(preference -> { 415 if (mTtsPlaybackManager != null) { 416 mTtsPlaybackManager.resetVoicePitch(); 417 mTtsPlaybackManager.resetSpeechRate(); 418 refreshUi(); 419 return true; 420 } 421 LOG.e("reset preference enabled before it is allowed"); 422 return false; 423 }); 424 425 // Initially disable. 426 resetPreference.setEnabled(false); 427 return resetPreference; 428 } 429 } 430