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