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