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