• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.tv.settings.system;
18 
19 import android.app.AlertDialog;
20 import android.content.ActivityNotFoundException;
21 import android.content.ContentResolver;
22 import android.content.Intent;
23 import android.os.Bundle;
24 import android.provider.Settings;
25 import android.speech.tts.TextToSpeech;
26 import android.speech.tts.TtsEngines;
27 import android.speech.tts.UtteranceProgressListener;
28 import android.support.annotation.Keep;
29 import android.support.v17.preference.LeanbackPreferenceFragment;
30 import android.support.v7.preference.ListPreference;
31 import android.support.v7.preference.Preference;
32 import android.support.v7.preference.PreferenceCategory;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import android.widget.Checkable;
36 
37 import com.android.tv.settings.R;
38 
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.MissingResourceException;
44 import java.util.Objects;
45 import java.util.Set;
46 
47 /**
48  * Fragment for TextToSpeech settings
49  */
50 @Keep
51 public class TextToSpeechFragment extends LeanbackPreferenceFragment  implements
52         Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
53         TtsEnginePreference.RadioButtonGroupState {
54     private static final String TAG = "TextToSpeechSettings";
55     private static final boolean DBG = false;
56 
57     /** Preference key for the engine settings preference */
58     private static final String KEY_ENGINE_SETTINGS = "tts_engine_settings";
59 
60     /** Preference key for the "play TTS example" preference. */
61     private static final String KEY_PLAY_EXAMPLE = "tts_play_example";
62 
63     /** Preference key for the TTS rate selection dialog. */
64     private static final String KEY_DEFAULT_RATE = "tts_default_rate";
65 
66     /** Preference key for the TTS status field. */
67     private static final String KEY_STATUS = "tts_status";
68 
69     /**
70      * Preference key for the engine selection preference.
71      */
72     private static final String KEY_ENGINE_PREFERENCE_SECTION =
73             "tts_engine_preference_section";
74 
75     /**
76      * These look like birth years, but they aren't mine. I'm much younger than this.
77      */
78     private static final int GET_SAMPLE_TEXT = 1983;
79     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
80 
81     private PreferenceCategory mEnginePreferenceCategory;
82     private Preference mEngineSettingsPref;
83     private ListPreference mDefaultRatePref;
84     private Preference mPlayExample;
85     private Preference mEngineStatus;
86 
87     private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
88 
89     /**
90      * The currently selected engine.
91      */
92     private String mCurrentEngine;
93 
94     /**
95      * The engine checkbox that is currently checked. Saves us a bit of effort
96      * in deducing the right one from the currently selected engine.
97      */
98     private Checkable mCurrentChecked;
99 
100     /**
101      * The previously selected TTS engine. Useful for rollbacks if the users
102      * choice is not loaded or fails a voice integrity check.
103      */
104     private String mPreviousEngine;
105 
106     private TextToSpeech mTts = null;
107     private TtsEngines mEnginesHelper = null;
108 
109     private String mSampleText = null;
110 
111     /**
112      * Default locale used by selected TTS engine, null if not connected to any engine.
113      */
114     private Locale mCurrentDefaultLocale;
115 
116     /**
117      * List of available locals of selected TTS engine, as returned by
118      * {@link TextToSpeech.Engine#ACTION_CHECK_TTS_DATA} activity. If empty, then activity
119      * was not yet called.
120      */
121     private List<String> mAvailableStrLocals;
122 
123     /**
124      * The initialization listener used when we are initalizing the settings
125      * screen for the first time (as opposed to when a user changes his choice
126      * of engine).
127      */
128     private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() {
129         @Override
130         public void onInit(int status) {
131             onInitEngine(status);
132         }
133     };
134 
135     /**
136      * The initialization listener used when the user changes his choice of
137      * engine (as opposed to when then screen is being initialized for the first
138      * time).
139      */
140     private final TextToSpeech.OnInitListener mUpdateListener = new TextToSpeech.OnInitListener() {
141         @Override
142         public void onInit(int status) {
143             onUpdateEngine(status);
144         }
145     };
146 
147     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)148     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
149         addPreferencesFromResource(R.xml.tts_settings);
150 
151         mEngineSettingsPref = findPreference(KEY_ENGINE_SETTINGS);
152 
153         mPlayExample = findPreference(KEY_PLAY_EXAMPLE);
154         mPlayExample.setOnPreferenceClickListener(this);
155         mPlayExample.setEnabled(false);
156 
157         mEnginePreferenceCategory = (PreferenceCategory) findPreference(
158                 KEY_ENGINE_PREFERENCE_SECTION);
159         mDefaultRatePref = (ListPreference) findPreference(KEY_DEFAULT_RATE);
160 
161         mEngineStatus = findPreference(KEY_STATUS);
162         updateEngineStatus(R.string.tts_status_checking);
163     }
164 
165     @Override
onCreate(Bundle savedInstanceState)166     public void onCreate(Bundle savedInstanceState) {
167         super.onCreate(savedInstanceState);
168 
169         getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
170 
171         mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
172         mEnginesHelper = new TtsEngines(getActivity().getApplicationContext());
173 
174         setTtsUtteranceProgressListener();
175         initSettings();
176     }
177 
178     @Override
onResume()179     public void onResume() {
180         super.onResume();
181 
182         if (mTts == null || mCurrentDefaultLocale == null) {
183             return;
184         }
185         Locale ttsDefaultLocale = mTts.getDefaultLanguage();
186         if (!mCurrentDefaultLocale.equals(ttsDefaultLocale)) {
187             updateWidgetState(false);
188             checkDefaultLocale();
189         }
190     }
191 
setTtsUtteranceProgressListener()192     private void setTtsUtteranceProgressListener() {
193         if (mTts == null) {
194             return;
195         }
196         mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
197             @Override
198             public void onStart(String utteranceId) {}
199 
200             @Override
201             public void onDone(String utteranceId) {}
202 
203             @Override
204             public void onError(String utteranceId) {
205                 Log.e(TAG, "Error while trying to synthesize sample text");
206             }
207         });
208     }
209 
210     @Override
onDestroy()211     public void onDestroy() {
212         super.onDestroy();
213         if (mTts != null) {
214             mTts.shutdown();
215             mTts = null;
216         }
217     }
218 
initSettings()219     private void initSettings() {
220         final ContentResolver resolver = getActivity().getContentResolver();
221 
222         // Set up the default rate.
223         try {
224             mDefaultRate = android.provider.Settings.Secure.getInt(resolver,
225                     Settings.Secure.TTS_DEFAULT_RATE);
226         } catch (Settings.SettingNotFoundException e) {
227             // Default rate setting not found, initialize it
228             mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
229         }
230         mDefaultRatePref.setValue(String.valueOf(mDefaultRate));
231         mDefaultRatePref.setOnPreferenceChangeListener(this);
232 
233         mCurrentEngine = mTts.getCurrentEngine();
234 
235         mEnginePreferenceCategory.removeAll();
236 
237         List<TextToSpeech.EngineInfo> engines = mEnginesHelper.getEngines();
238         for (TextToSpeech.EngineInfo engine : engines) {
239             TtsEnginePreference enginePref =
240                     new TtsEnginePreference(getPreferenceManager().getContext(), engine,
241                     this);
242             mEnginePreferenceCategory.addPreference(enginePref);
243         }
244 
245         checkVoiceData(mCurrentEngine);
246     }
247 
248     /**
249      * Called when the TTS engine is initialized.
250      */
onInitEngine(int status)251     public void onInitEngine(int status) {
252         if (status == TextToSpeech.SUCCESS) {
253             if (DBG) Log.d(TAG, "TTS engine for settings screen initialized.");
254             checkDefaultLocale();
255         } else {
256             if (DBG) Log.d(TAG, "TTS engine for settings screen failed to initialize successfully.");
257             updateWidgetState(false);
258         }
259     }
260 
checkDefaultLocale()261     private void checkDefaultLocale() {
262         Locale defaultLocale = mTts.getDefaultLanguage();
263         if (defaultLocale == null) {
264             Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine);
265             updateWidgetState(false);
266             updateEngineStatus(R.string.tts_status_not_supported);
267             return;
268         }
269 
270         // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize,
271         // we may end up with English (USA)and German (DEU).
272         final Locale oldDefaultLocale = mCurrentDefaultLocale;
273         mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString());
274         if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) {
275             mSampleText = null;
276         }
277 
278         mTts.setLanguage(defaultLocale);
279         if (evaluateDefaultLocale() && mSampleText == null) {
280             getSampleText();
281         }
282     }
283 
evaluateDefaultLocale()284     private boolean evaluateDefaultLocale() {
285         // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list
286         // of available languages.
287         if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) {
288             return false;
289         }
290 
291         boolean notInAvailableLangauges = true;
292         try {
293             // Check if language is listed in CheckVoices Action result as available voice.
294             String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language();
295             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) {
296                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country();
297             }
298             if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) {
299                 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant();
300             }
301 
302             for (String loc : mAvailableStrLocals) {
303                 if (loc.equalsIgnoreCase(defaultLocaleStr)) {
304                     notInAvailableLangauges = false;
305                     break;
306                 }
307             }
308         } catch (MissingResourceException e) {
309             if (DBG) Log.wtf(TAG, "MissingResourceException", e);
310             updateEngineStatus(R.string.tts_status_not_supported);
311             updateWidgetState(false);
312             return false;
313         }
314 
315         int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale);
316         if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED ||
317                 defaultAvailable == TextToSpeech.LANG_MISSING_DATA ||
318                 notInAvailableLangauges) {
319             if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported.");
320             updateEngineStatus(R.string.tts_status_not_supported);
321             updateWidgetState(false);
322             return false;
323         } else {
324             if (isNetworkRequiredForSynthesis()) {
325                 updateEngineStatus(R.string.tts_status_requires_network);
326             } else {
327                 updateEngineStatus(R.string.tts_status_ok);
328             }
329             updateWidgetState(true);
330             return true;
331         }
332     }
333 
334     /**
335      * Ask the current default engine to return a string of sample text to be
336      * spoken to the user.
337      */
getSampleText()338     private void getSampleText() {
339         String currentEngine = mTts.getCurrentEngine();
340 
341         if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine();
342 
343         // TODO: This is currently a hidden private API. The intent extras
344         // and the intent action should be made public if we intend to make this
345         // a public API. We fall back to using a canned set of strings if this
346         // doesn't work.
347         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
348 
349         intent.putExtra("language", mCurrentDefaultLocale.getLanguage());
350         intent.putExtra("country", mCurrentDefaultLocale.getCountry());
351         intent.putExtra("variant", mCurrentDefaultLocale.getVariant());
352         intent.setPackage(currentEngine);
353 
354         try {
355             if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0));
356             startActivityForResult(intent, GET_SAMPLE_TEXT);
357         } catch (ActivityNotFoundException ex) {
358             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
359         }
360     }
361 
362     /**
363      * Called when voice data integrity check returns
364      */
365     @Override
onActivityResult(int requestCode, int resultCode, Intent data)366     public void onActivityResult(int requestCode, int resultCode, Intent data) {
367         if (requestCode == GET_SAMPLE_TEXT) {
368             onSampleTextReceived(resultCode, data);
369         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
370             onVoiceDataIntegrityCheckDone(data);
371         }
372     }
373 
getDefaultSampleString()374     private String getDefaultSampleString() {
375         if (mTts != null && mTts.getLanguage() != null) {
376             try {
377                 final String currentLang = mTts.getLanguage().getISO3Language();
378                 String[] strings = getActivity().getResources().getStringArray(
379                         R.array.tts_demo_strings);
380                 String[] langs = getActivity().getResources().getStringArray(
381                         R.array.tts_demo_string_langs);
382 
383                 for (int i = 0; i < strings.length; ++i) {
384                     if (langs[i].equals(currentLang)) {
385                         return strings[i];
386                     }
387                 }
388             } catch (MissingResourceException e) {
389                 if (DBG) Log.wtf(TAG, "MissingResourceException", e);
390                 // Ignore and fall back to default sample string
391             }
392         }
393         return getString(R.string.tts_default_sample_string);
394     }
395 
isNetworkRequiredForSynthesis()396     private boolean isNetworkRequiredForSynthesis() {
397         Set<String> features = mTts.getFeatures(mCurrentDefaultLocale);
398         return features != null &&
399                 features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
400                 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
401     }
402 
onSampleTextReceived(int resultCode, Intent data)403     private void onSampleTextReceived(int resultCode, Intent data) {
404         String sample = getDefaultSampleString();
405 
406         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
407             if (data.getStringExtra("sampleText") != null) {
408                 sample = data.getStringExtra("sampleText");
409             }
410             if (DBG) Log.d(TAG, "Got sample text: " + sample);
411         } else {
412             if (DBG) Log.d(TAG, "Using default sample text :" + sample);
413         }
414 
415         mSampleText = sample;
416         if (mSampleText != null) {
417             updateWidgetState(true);
418         } else {
419             Log.e(TAG, "Did not have a sample string for the requested language. Using default");
420         }
421     }
422 
speakSampleText()423     private void speakSampleText() {
424         final boolean networkRequired = isNetworkRequiredForSynthesis();
425         if (!networkRequired ||
426                 mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE) {
427             HashMap<String, String> params = new HashMap<>();
428             params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample");
429 
430             mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params);
431         } else {
432             Log.w(TAG, "Network required for sample synthesis for requested language");
433             displayNetworkAlert();
434         }
435     }
436 
437     @Override
onPreferenceChange(Preference preference, Object objValue)438     public boolean onPreferenceChange(Preference preference, Object objValue) {
439         if (KEY_DEFAULT_RATE.equals(preference.getKey())) {
440             // Default rate
441             mDefaultRate = Integer.parseInt((String) objValue);
442             try {
443                 android.provider.Settings.Secure.putInt(getActivity().getContentResolver(),
444                         Settings.Secure.TTS_DEFAULT_RATE, mDefaultRate);
445                 if (mTts != null) {
446                     mTts.setSpeechRate(mDefaultRate / 100.0f);
447                 }
448                 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate);
449             } catch (NumberFormatException e) {
450                 Log.e(TAG, "could not persist default TTS rate setting", e);
451             }
452         }
453 
454         return true;
455     }
456 
457     /**
458      * Called when mPlayExample is clicked
459      */
460     @Override
onPreferenceClick(Preference preference)461     public boolean onPreferenceClick(Preference preference) {
462         if (preference == mPlayExample) {
463             // Get the sample text from the TTS engine; onActivityResult will do
464             // the actual speaking
465             speakSampleText();
466             return true;
467         }
468 
469         return false;
470     }
471 
updateWidgetState(boolean enable)472     private void updateWidgetState(boolean enable) {
473         mEngineSettingsPref.setEnabled(enable);
474         mPlayExample.setEnabled(enable);
475         mDefaultRatePref.setEnabled(enable);
476         mEngineStatus.setEnabled(enable);
477     }
478 
updateEngineStatus(int resourceId)479     private void updateEngineStatus(int resourceId) {
480         Locale locale = mCurrentDefaultLocale;
481         if (locale == null) {
482             locale = Locale.getDefault();
483         }
484         mEngineStatus.setSummary(getString(resourceId, locale.getDisplayName()));
485     }
486 
displayNetworkAlert()487     private void displayNetworkAlert() {
488         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
489         builder.setTitle(android.R.string.dialog_alert_title)
490                 .setMessage(getActivity().getString(R.string.tts_engine_network_required))
491                 .setCancelable(false)
492                 .setPositiveButton(android.R.string.ok, null);
493 
494         AlertDialog dialog = builder.create();
495         dialog.show();
496     }
497 
updateDefaultEngine(String engine)498     private void updateDefaultEngine(String engine) {
499         if (DBG) Log.d(TAG, "Updating default synth to : " + engine);
500 
501         // Disable the "play sample text" preference and the speech
502         // rate preference while the engine is being swapped.
503         updateWidgetState(false);
504         updateEngineStatus(R.string.tts_status_checking);
505 
506         // Keep track of the previous engine that was being used. So that
507         // we can reuse the previous engine.
508         //
509         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
510         // the very least that we successfully bound to the engine service.
511         mPreviousEngine = mTts.getCurrentEngine();
512 
513         // Step 1: Shut down the existing TTS engine.
514         try {
515             mTts.shutdown();
516             mTts = null;
517         } catch (Exception e) {
518             Log.e(TAG, "Error shutting down TTS engine" + e);
519         }
520 
521         // Step 2: Connect to the new TTS engine.
522         // Step 3 is continued on #onUpdateEngine (below) which is called when
523         // the app binds successfully to the engine.
524         if (DBG) Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine);
525         mTts = new TextToSpeech(getActivity().getApplicationContext(), mUpdateListener, engine);
526         setTtsUtteranceProgressListener();
527     }
528 
529     /*
530      * Step 3: We have now bound to the TTS engine the user requested. We will
531      * attempt to check voice data for the engine if we successfully bound to it,
532      * or revert to the previous engine if we didn't.
533      */
onUpdateEngine(int status)534     public void onUpdateEngine(int status) {
535         if (status == TextToSpeech.SUCCESS) {
536             if (DBG) {
537                 Log.d(TAG, "Updating engine: Successfully bound to the engine: " +
538                         mTts.getCurrentEngine());
539             }
540             checkVoiceData(mTts.getCurrentEngine());
541         } else {
542             if (DBG) Log.d(TAG, "Updating engine: Failed to bind to engine, reverting.");
543             if (mPreviousEngine != null) {
544                 // This is guaranteed to at least bind, since mPreviousEngine would be
545                 // null if the previous bind to this engine failed.
546                 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener,
547                         mPreviousEngine);
548                 setTtsUtteranceProgressListener();
549             }
550             mPreviousEngine = null;
551         }
552     }
553 
554     /*
555      * Step 4: Check whether the voice data for the engine is ok.
556      */
checkVoiceData(String engine)557     private void checkVoiceData(String engine) {
558         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
559         intent.setPackage(engine);
560         try {
561             if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
562             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
563         } catch (ActivityNotFoundException ex) {
564             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
565         }
566     }
567 
568     /*
569      * Step 5: The voice data check is complete.
570      */
onVoiceDataIntegrityCheckDone(Intent data)571     private void onVoiceDataIntegrityCheckDone(Intent data) {
572         final String engine = mTts.getCurrentEngine();
573 
574         if (engine == null) {
575             Log.e(TAG, "Voice data check complete, but no engine bound");
576             return;
577         }
578 
579         if (data == null){
580             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
581                     mTts.getCurrentEngine());
582             return;
583         }
584 
585         android.provider.Settings.Secure.putString(getActivity().getContentResolver(),
586                 Settings.Secure.TTS_DEFAULT_SYNTH, engine);
587 
588         mAvailableStrLocals = data.getStringArrayListExtra(
589                 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
590         if (mAvailableStrLocals == null) {
591             Log.e(TAG, "Voice data check complete, but no available voices found");
592             // Set mAvailableStrLocals to empty list
593             mAvailableStrLocals = new ArrayList<String>();
594         }
595         if (evaluateDefaultLocale()) {
596             getSampleText();
597         }
598 
599         final TextToSpeech.EngineInfo engineInfo = mEnginesHelper.getEngineInfo(engine);
600         TtsEngineSettingsFragment.prepareArgs(mEngineSettingsPref.getExtras(),
601                 engineInfo.name, engineInfo.label, data);
602     }
603 
604     @Override
getCurrentChecked()605     public Checkable getCurrentChecked() {
606         return mCurrentChecked;
607     }
608 
609     @Override
getCurrentKey()610     public String getCurrentKey() {
611         return mCurrentEngine;
612     }
613 
614     @Override
setCurrentChecked(Checkable current)615     public void setCurrentChecked(Checkable current) {
616         mCurrentChecked = current;
617     }
618 
619     @Override
setCurrentKey(String key)620     public void setCurrentKey(String key) {
621         mCurrentEngine = key;
622         updateDefaultEngine(mCurrentEngine);
623     }
624 
625 
626 }
627