/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.tts; import android.car.drivingstate.CarUxRestrictions; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.speech.tts.TtsEngines; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import com.android.car.settings.R; import com.android.car.settings.common.ActivityResultCallback; import com.android.car.settings.common.FragmentController; import com.android.car.settings.common.Logger; import com.android.car.settings.common.PreferenceController; import com.android.car.settings.common.SeekBarPreference; import java.util.ArrayList; import java.util.Collections; import java.util.Locale; import java.util.Objects; /** * Business logic for configuring and listening to the current TTS voice. This preference controller * handles the following: * *
    *
  1. Changing the TTS language *
  2. Changing the TTS speech rate *
  3. Changing the TTS voice pitch *
  4. Resetting the TTS configuration *
*/ public class TtsPlaybackPreferenceController extends PreferenceController implements ActivityResultCallback { private static final Logger LOG = new Logger(TtsPlaybackPreferenceController.class); @VisibleForTesting static final int VOICE_DATA_CHECK = 1; @VisibleForTesting static final int GET_SAMPLE_TEXT = 2; private TtsEngines mEnginesHelper; private TtsPlaybackSettingsManager mTtsPlaybackManager; private TextToSpeech mTts; private int mSelectedLocaleIndex; private ListPreference mDefaultLanguagePreference; private SeekBarPreference mSpeechRatePreference; private SeekBarPreference mVoicePitchPreference; private Preference mResetPreference; private String mSampleText; private Locale mSampleTextLocale; private Handler mUiHandler; @VisibleForTesting Handler mBackgroundHandler; private HandlerThread mBackgroundHandlerThread; /** True if initialized with no errors. */ private boolean mTtsInitialized = false; private final TextToSpeech.OnInitListener mOnInitListener = status -> { if (status == TextToSpeech.SUCCESS) { mTtsInitialized = true; mTtsPlaybackManager = new TtsPlaybackSettingsManager(getContext(), mTts, mEnginesHelper); mTts.setSpeechRate(mTtsPlaybackManager.getCurrentSpeechRate() / TtsPlaybackSettingsManager.SCALING_FACTOR); mTts.setPitch(mTtsPlaybackManager.getCurrentVoicePitch() / TtsPlaybackSettingsManager.SCALING_FACTOR); startEngineVoiceDataCheck(mTts.getCurrentEngine()); mBackgroundHandler.post(() -> { checkOrUpdateSampleText(); mUiHandler.post(this::refreshUi); }); } }; public TtsPlaybackPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions) { super(context, preferenceKey, fragmentController, uxRestrictions); mEnginesHelper = new TtsEngines(context); } @Override protected Class getPreferenceType() { return PreferenceGroup.class; } @Override protected void onCreateInternal() { mDefaultLanguagePreference = initDefaultLanguagePreference(); mSpeechRatePreference = initSpeechRatePreference(); mVoicePitchPreference = initVoicePitchPreference(); mResetPreference = initResetTtsPlaybackPreference(); mUiHandler = new Handler(Looper.getMainLooper()); mBackgroundHandlerThread = new HandlerThread(/* name= */"BackgroundHandlerThread"); mBackgroundHandlerThread.start(); mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper()); mTts = new TextToSpeech(getContext(), mOnInitListener); } @Override protected void onDestroyInternal() { if (mBackgroundHandlerThread != null) { mBackgroundHandlerThread.quit(); mBackgroundHandler = null; mBackgroundHandlerThread = null; } if (mTts != null) { mTts.shutdown(); mTts = null; mTtsPlaybackManager = null; } } @Override protected void updateState(PreferenceGroup preference) { boolean isValid = isDefaultLocaleValid(); mDefaultLanguagePreference.setEnabled(isValid); // Always hide default language preference for now. // TODO: Unhide once product requirements are clarified. mDefaultLanguagePreference.setVisible(false); mSpeechRatePreference.setEnabled(isValid); mVoicePitchPreference.setEnabled(isValid); mResetPreference.setEnabled(isValid); if (!isValid && mDefaultLanguagePreference.getEntries() != null) { mDefaultLanguagePreference.setEnabled(true); } if (mDefaultLanguagePreference.getEntries() != null) { mDefaultLanguagePreference.setValueIndex(mSelectedLocaleIndex); mDefaultLanguagePreference.setSummary( mDefaultLanguagePreference.getEntries()[mSelectedLocaleIndex]); } if (mTtsInitialized) { mSpeechRatePreference.setValue(mTtsPlaybackManager.getCurrentSpeechRate()); mVoicePitchPreference.setValue(mTtsPlaybackManager.getCurrentVoicePitch()); } } @Override public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) { switch (requestCode) { case VOICE_DATA_CHECK: onVoiceDataIntegrityCheckDone(resultCode, data); break; case GET_SAMPLE_TEXT: onSampleTextReceived(resultCode, data); break; default: LOG.e("Got unknown activity result"); } } private void startEngineVoiceDataCheck(String engine) { Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); intent.setPackage(engine); try { LOG.d("Updating engine: Checking voice data: " + intent.toUri(0)); getFragmentController().startActivityForResult(intent, VOICE_DATA_CHECK, this); } catch (ActivityNotFoundException ex) { LOG.e("Failed to check TTS data, no activity found for " + intent); } } /** * Ask the current default engine to return a string of sample text to be * spoken to the user. */ private void startGetSampleText() { String currentEngine = mTts.getCurrentEngine(); if (TextUtils.isEmpty(currentEngine)) { currentEngine = mTts.getDefaultEngine(); } Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); mSampleTextLocale = mTtsPlaybackManager.getEffectiveTtsLocale(); if (mSampleTextLocale == null) { return; } intent.putExtra(TextToSpeech.Engine.KEY_PARAM_LANGUAGE, mSampleTextLocale.getLanguage()); intent.putExtra(TextToSpeech.Engine.KEY_PARAM_COUNTRY, mSampleTextLocale.getCountry()); intent.putExtra(TextToSpeech.Engine.KEY_PARAM_VARIANT, mSampleTextLocale.getVariant()); intent.setPackage(currentEngine); try { LOG.d("Getting sample text: " + intent.toUri(0)); getFragmentController().startActivityForResult(intent, GET_SAMPLE_TEXT, this); } catch (ActivityNotFoundException ex) { LOG.e("Failed to get sample text, no activity found for " + intent + ")"); } } /** The voice data check is complete. */ private void onVoiceDataIntegrityCheckDone(int resultCode, Intent data) { String engine = mTts.getCurrentEngine(); if (engine == null) { LOG.e("Voice data check complete, but no engine bound"); return; } if (data == null || resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) { LOG.e("Engine failed voice data integrity check (null return or invalid result code)" + mTts.getCurrentEngine()); return; } Settings.Secure.putString(getContext().getContentResolver(), Settings.Secure.TTS_DEFAULT_SYNTH, engine); ArrayList availableLangs = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); if (availableLangs == null || availableLangs.size() == 0) { refreshUi(); return; } updateDefaultLanguagePreference(availableLangs); mSelectedLocaleIndex = findLocaleIndex(mTtsPlaybackManager.getStoredTtsLocale()); if (mSelectedLocaleIndex < 0) { mSelectedLocaleIndex = 0; } mBackgroundHandler.post(() -> { startGetSampleText(); mUiHandler.post(this::refreshUi); }); } private void onSampleTextReceived(int resultCode, Intent data) { String sample = getContext().getString(R.string.tts_default_sample_string); if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) { String tmp = data.getStringExtra(TextToSpeech.Engine.EXTRA_SAMPLE_TEXT); if (!TextUtils.isEmpty(tmp)) { sample = tmp; } LOG.d("Got sample text: " + sample); } else { LOG.d("Using default sample text :" + sample); } mSampleText = sample; } private void updateLanguageTo(Locale locale) { int selectedLocaleIndex = findLocaleIndex(locale); if (selectedLocaleIndex == -1) { LOG.w("updateLanguageTo called with unknown locale argument"); return; } if (mTtsPlaybackManager.updateTtsLocale(locale)) { mSelectedLocaleIndex = selectedLocaleIndex; checkOrUpdateSampleText(); refreshUi(); } else { LOG.e("updateLanguageTo failed to update tts language"); } } private int findLocaleIndex(Locale locale) { String localeString = (locale != null) ? locale.toString() : ""; return mDefaultLanguagePreference.findIndexOfValue(localeString); } private boolean isDefaultLocaleValid() { if (!mTtsInitialized) { return false; } if (mSampleTextLocale == null) { LOG.e("Default language was not retrieved from engine " + mTts.getCurrentEngine()); return false; } if (mDefaultLanguagePreference.getEntries() == null) { return false; } int index = mDefaultLanguagePreference.findIndexOfValue(mSampleTextLocale.toString()); if (index < 0) { return false; } return true; } private void checkOrUpdateSampleText() { if (!mTtsInitialized) { return; } Locale defaultLocale = mTtsPlaybackManager.getEffectiveTtsLocale(); if (defaultLocale == null) { LOG.e("Failed to get default language from engine " + mTts.getCurrentEngine()); return; } if (!Objects.equals(defaultLocale, mSampleTextLocale)) { mSampleText = null; mSampleTextLocale = null; } if (mSampleText == null) { startGetSampleText(); } } @VisibleForTesting String getSampleText() { return mSampleText; } /* ***************************************************************************************** * * Preference initialization/update code. * * ***************************************************************************************** */ private ListPreference initDefaultLanguagePreference() { ListPreference defaultLanguagePreference = (ListPreference) getPreference().findPreference( getContext().getString(R.string.pk_tts_default_language)); defaultLanguagePreference.setOnPreferenceChangeListener((preference, newValue) -> { String localeString = (String) newValue; updateLanguageTo(!TextUtils.isEmpty(localeString) ? mEnginesHelper.parseLocaleString( localeString) : null); checkOrUpdateSampleText(); return true; }); return defaultLanguagePreference; } private void updateDefaultLanguagePreference(@NonNull ArrayList availableLangs) { // Sort locales by display name. ArrayList locales = new ArrayList<>(); for (int i = 0; i < availableLangs.size(); i++) { Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i)); if (locale != null) { locales.add(locale); } } Collections.sort(locales, (lhs, rhs) -> lhs.getDisplayName().compareToIgnoreCase(rhs.getDisplayName())); // Separate pairs into two separate arrays. CharSequence[] entries = new CharSequence[availableLangs.size() + 1]; CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1]; entries[0] = getContext().getString(R.string.tts_lang_use_system); entryValues[0] = ""; int i = 1; for (Locale locale : locales) { entries[i] = locale.getDisplayName(); entryValues[i++] = locale.toString(); } mDefaultLanguagePreference.setEntries(entries); mDefaultLanguagePreference.setEntryValues(entryValues); } private SeekBarPreference initSpeechRatePreference() { SeekBarPreference speechRatePreference = (SeekBarPreference) getPreference().findPreference( getContext().getString(R.string.pk_tts_speech_rate)); speechRatePreference.setMin(TtsPlaybackSettingsManager.MIN_SPEECH_RATE); speechRatePreference.setMax(TtsPlaybackSettingsManager.MAX_SPEECH_RATE); speechRatePreference.setShowSeekBarValue(false); speechRatePreference.setContinuousUpdate(false); speechRatePreference.setOnPreferenceChangeListener((preference, newValue) -> { if (mTtsPlaybackManager != null) { mTtsPlaybackManager.updateSpeechRate((Integer) newValue); mTtsPlaybackManager.speakSampleText(mSampleText); return true; } LOG.e("speech rate preference enabled before it is allowed"); return false; }); // Initially disable. speechRatePreference.setEnabled(false); return speechRatePreference; } private SeekBarPreference initVoicePitchPreference() { SeekBarPreference pitchPreference = (SeekBarPreference) getPreference().findPreference( getContext().getString(R.string.pk_tts_pitch)); pitchPreference.setMin(TtsPlaybackSettingsManager.MIN_VOICE_PITCH); pitchPreference.setMax(TtsPlaybackSettingsManager.MAX_VOICE_PITCH); pitchPreference.setShowSeekBarValue(false); pitchPreference.setContinuousUpdate(false); pitchPreference.setOnPreferenceChangeListener((preference, newValue) -> { if (mTtsPlaybackManager != null) { mTtsPlaybackManager.updateVoicePitch((Integer) newValue); mTtsPlaybackManager.speakSampleText(mSampleText); return true; } LOG.e("speech pitch preference enabled before it is allowed"); return false; }); // Initially disable. pitchPreference.setEnabled(false); return pitchPreference; } private Preference initResetTtsPlaybackPreference() { Preference resetPreference = getPreference().findPreference( getContext().getString(R.string.pk_tts_reset)); resetPreference.setOnPreferenceClickListener(preference -> { if (mTtsPlaybackManager != null) { mTtsPlaybackManager.resetVoicePitch(); mTtsPlaybackManager.resetSpeechRate(); refreshUi(); return true; } LOG.e("reset preference enabled before it is allowed"); return false; }); // Initially disable. resetPreference.setEnabled(false); return resetPreference; } }