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