• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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