• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.provider.Settings.Secure.TTS_DEFAULT_RATE;
20 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
21 
22 import com.android.settings.R;
23 import com.android.settings.SettingsPreferenceFragment;
24 import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState;
25 
26 import android.app.AlertDialog;
27 import android.content.ActivityNotFoundException;
28 import android.content.ContentResolver;
29 import android.content.DialogInterface;
30 import android.content.Intent;
31 import android.os.Bundle;
32 import android.preference.ListPreference;
33 import android.preference.Preference;
34 import android.preference.PreferenceActivity;
35 import android.preference.PreferenceCategory;
36 import android.provider.Settings;
37 import android.provider.Settings.SettingNotFoundException;
38 import android.speech.tts.TextToSpeech;
39 import android.speech.tts.TextToSpeech.EngineInfo;
40 import android.speech.tts.TtsEngines;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.widget.Checkable;
44 
45 import java.util.List;
46 import java.util.Locale;
47 
48 public class TextToSpeechSettings extends SettingsPreferenceFragment implements
49         Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
50         RadioButtonGroupState {
51 
52     private static final String TAG = "TextToSpeechSettings";
53     private static final boolean DBG = false;
54 
55     /** Preference key for the "play TTS example" preference. */
56     private static final String KEY_PLAY_EXAMPLE = "tts_play_example";
57 
58     /** Preference key for the TTS rate selection dialog. */
59     private static final String KEY_DEFAULT_RATE = "tts_default_rate";
60 
61     /**
62      * Preference key for the engine selection preference.
63      */
64     private static final String KEY_ENGINE_PREFERENCE_SECTION =
65             "tts_engine_preference_section";
66 
67     /**
68      * These look like birth years, but they aren't mine. I'm much younger than this.
69      */
70     private static final int GET_SAMPLE_TEXT = 1983;
71     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
72 
73     private PreferenceCategory mEnginePreferenceCategory;
74     private ListPreference mDefaultRatePref;
75     private Preference mPlayExample;
76 
77     private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
78 
79     /**
80      * The currently selected engine.
81      */
82     private String mCurrentEngine;
83 
84     /**
85      * The engine checkbox that is currently checked. Saves us a bit of effort
86      * in deducing the right one from the currently selected engine.
87      */
88     private Checkable mCurrentChecked;
89 
90     /**
91      * The previously selected TTS engine. Useful for rollbacks if the users
92      * choice is not loaded or fails a voice integrity check.
93      */
94     private String mPreviousEngine;
95 
96     private TextToSpeech mTts = null;
97     private TtsEngines mEnginesHelper = null;
98 
99     /**
100      * The initialization listener used when we are initalizing the settings
101      * screen for the first time (as opposed to when a user changes his choice
102      * of engine).
103      */
104     private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() {
105         @Override
106         public void onInit(int status) {
107             onInitEngine(status);
108         }
109     };
110 
111     /**
112      * The initialization listener used when the user changes his choice of
113      * engine (as opposed to when then screen is being initialized for the first
114      * time).
115      */
116     private final TextToSpeech.OnInitListener mUpdateListener = new TextToSpeech.OnInitListener() {
117         @Override
118         public void onInit(int status) {
119             onUpdateEngine(status);
120         }
121     };
122 
123     @Override
onCreate(Bundle savedInstanceState)124     public void onCreate(Bundle savedInstanceState) {
125         super.onCreate(savedInstanceState);
126         addPreferencesFromResource(R.xml.tts_settings);
127 
128         getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
129 
130         mPlayExample = findPreference(KEY_PLAY_EXAMPLE);
131         mPlayExample.setOnPreferenceClickListener(this);
132 
133         mEnginePreferenceCategory = (PreferenceCategory) findPreference(
134                 KEY_ENGINE_PREFERENCE_SECTION);
135         mDefaultRatePref = (ListPreference) findPreference(KEY_DEFAULT_RATE);
136 
137         mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
138         mEnginesHelper = new TtsEngines(getActivity().getApplicationContext());
139 
140         initSettings();
141     }
142 
143     @Override
onDestroy()144     public void onDestroy() {
145         super.onDestroy();
146         if (mTts != null) {
147             mTts.shutdown();
148             mTts = null;
149         }
150     }
151 
152     @Override
onPause()153     public void onPause() {
154         super.onPause();
155         if ((mDefaultRatePref != null) && (mDefaultRatePref.getDialog() != null)) {
156             mDefaultRatePref.getDialog().dismiss();
157         }
158     }
159 
initSettings()160     private void initSettings() {
161         final ContentResolver resolver = getContentResolver();
162 
163         // Set up the default rate.
164         try {
165             mDefaultRate = Settings.Secure.getInt(resolver, TTS_DEFAULT_RATE);
166         } catch (SettingNotFoundException e) {
167             // Default rate setting not found, initialize it
168             mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
169         }
170         mDefaultRatePref.setValue(String.valueOf(mDefaultRate));
171         mDefaultRatePref.setOnPreferenceChangeListener(this);
172 
173         mCurrentEngine = mTts.getCurrentEngine();
174 
175         PreferenceActivity preferenceActivity = null;
176         if (getActivity() instanceof PreferenceActivity) {
177             preferenceActivity = (PreferenceActivity) getActivity();
178         } else {
179             throw new IllegalStateException("TextToSpeechSettings used outside a " +
180                     "PreferenceActivity");
181         }
182 
183         mEnginePreferenceCategory.removeAll();
184 
185         List<EngineInfo> engines = mEnginesHelper.getEngines();
186         for (EngineInfo engine : engines) {
187             TtsEnginePreference enginePref = new TtsEnginePreference(getActivity(), engine,
188                     this, preferenceActivity);
189             mEnginePreferenceCategory.addPreference(enginePref);
190         }
191 
192         checkVoiceData(mCurrentEngine);
193     }
194 
195     /**
196      * Ask the current default engine to return a string of sample text to be
197      * spoken to the user.
198      */
getSampleText()199     private void getSampleText() {
200         String currentEngine = mTts.getCurrentEngine();
201 
202         if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine();
203 
204         Locale currentLocale = mTts.getLanguage();
205 
206         // TODO: This is currently a hidden private API. The intent extras
207         // and the intent action should be made public if we intend to make this
208         // a public API. We fall back to using a canned set of strings if this
209         // doesn't work.
210         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
211 
212         if (currentLocale != null) {
213             intent.putExtra("language", currentLocale.getLanguage());
214             intent.putExtra("country", currentLocale.getCountry());
215             intent.putExtra("variant", currentLocale.getVariant());
216         }
217         intent.setPackage(currentEngine);
218 
219         try {
220             if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0));
221             startActivityForResult(intent, GET_SAMPLE_TEXT);
222         } catch (ActivityNotFoundException ex) {
223             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
224         }
225     }
226 
227     /**
228      * Called when the TTS engine is initialized.
229      */
onInitEngine(int status)230     public void onInitEngine(int status) {
231         if (status == TextToSpeech.SUCCESS) {
232             updateWidgetState(true);
233             if (DBG) Log.d(TAG, "TTS engine for settings screen initialized.");
234         } else {
235             if (DBG) Log.d(TAG, "TTS engine for settings screen failed to initialize successfully.");
236             updateWidgetState(false);
237         }
238     }
239 
240     /**
241      * Called when voice data integrity check returns
242      */
243     @Override
onActivityResult(int requestCode, int resultCode, Intent data)244     public void onActivityResult(int requestCode, int resultCode, Intent data) {
245         if (requestCode == GET_SAMPLE_TEXT) {
246             onSampleTextReceived(resultCode, data);
247         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
248             onVoiceDataIntegrityCheckDone(data);
249         }
250     }
251 
getDefaultSampleString()252     private String getDefaultSampleString() {
253         if (mTts != null && mTts.getLanguage() != null) {
254             final String currentLang = mTts.getLanguage().getISO3Language();
255             String[] strings = getActivity().getResources().getStringArray(
256                     R.array.tts_demo_strings);
257             String[] langs = getActivity().getResources().getStringArray(
258                     R.array.tts_demo_string_langs);
259 
260             for (int i = 0; i < strings.length; ++i) {
261                 if (langs[i].equals(currentLang)) {
262                     return strings[i];
263                 }
264             }
265         }
266         return null;
267     }
268 
onSampleTextReceived(int resultCode, Intent data)269     private void onSampleTextReceived(int resultCode, Intent data) {
270         String sample = getDefaultSampleString();
271 
272         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
273             if (data != null && data.getStringExtra("sampleText") != null) {
274                 sample = data.getStringExtra("sampleText");
275             }
276             if (DBG) Log.d(TAG, "Got sample text: " + sample);
277         } else {
278             if (DBG) Log.d(TAG, "Using default sample text :" + sample);
279         }
280 
281         if (sample != null && mTts != null) {
282             // The engine is guaranteed to have been initialized here
283             // because this preference is not enabled otherwise.
284             mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, null);
285         } else {
286             // TODO: Display an error here to the user.
287             Log.e(TAG, "Did not have a sample string for the requested language");
288         }
289     }
290 
onPreferenceChange(Preference preference, Object objValue)291     public boolean onPreferenceChange(Preference preference, Object objValue) {
292         if (KEY_DEFAULT_RATE.equals(preference.getKey())) {
293             // Default rate
294             mDefaultRate = Integer.parseInt((String) objValue);
295             try {
296                 Settings.Secure.putInt(getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate);
297                 if (mTts != null) {
298                     mTts.setSpeechRate(mDefaultRate / 100.0f);
299                 }
300                 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate);
301             } catch (NumberFormatException e) {
302                 Log.e(TAG, "could not persist default TTS rate setting", e);
303             }
304         }
305 
306         return true;
307     }
308 
309     /**
310      * Called when mPlayExample is clicked
311      */
onPreferenceClick(Preference preference)312     public boolean onPreferenceClick(Preference preference) {
313         if (preference == mPlayExample) {
314             // Get the sample text from the TTS engine; onActivityResult will do
315             // the actual speaking
316             getSampleText();
317             return true;
318         }
319 
320         return false;
321     }
322 
updateWidgetState(boolean enable)323     private void updateWidgetState(boolean enable) {
324         mPlayExample.setEnabled(enable);
325         mDefaultRatePref.setEnabled(enable);
326     }
327 
displayDataAlert(final String key)328     private void displayDataAlert(final String key) {
329         Log.i(TAG, "Displaying data alert for :" + key);
330         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
331         builder.setTitle(android.R.string.dialog_alert_title);
332         builder.setIcon(android.R.drawable.ic_dialog_alert);
333         builder.setMessage(getActivity().getString(
334                 R.string.tts_engine_security_warning, mEnginesHelper.getEngineInfo(key).label));
335         builder.setCancelable(true);
336         builder.setPositiveButton(android.R.string.ok,
337                 new DialogInterface.OnClickListener() {
338                     public void onClick(DialogInterface dialog, int which) {
339                        updateDefaultEngine(key);
340                     }
341                 });
342         builder.setNegativeButton(android.R.string.cancel, null);
343 
344         AlertDialog dialog = builder.create();
345         dialog.show();
346     }
347 
updateDefaultEngine(String engine)348     private void updateDefaultEngine(String engine) {
349         if (DBG) Log.d(TAG, "Updating default synth to : " + engine);
350 
351         // Disable the "play sample text" preference and the speech
352         // rate preference while the engine is being swapped.
353         updateWidgetState(false);
354 
355         // Keep track of the previous engine that was being used. So that
356         // we can reuse the previous engine.
357         //
358         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
359         // the very least that we successfully bound to the engine service.
360         mPreviousEngine = mTts.getCurrentEngine();
361 
362         // Step 1: Shut down the existing TTS engine.
363         if (mTts != null) {
364             try {
365                 mTts.shutdown();
366                 mTts = null;
367             } catch (Exception e) {
368                 Log.e(TAG, "Error shutting down TTS engine" + e);
369             }
370         }
371 
372         // Step 2: Connect to the new TTS engine.
373         // Step 3 is continued on #onUpdateEngine (below) which is called when
374         // the app binds successfully to the engine.
375         if (DBG) Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine);
376         mTts = new TextToSpeech(getActivity().getApplicationContext(), mUpdateListener, engine);
377     }
378 
379     /*
380      * Step 3: We have now bound to the TTS engine the user requested. We will
381      * attempt to check voice data for the engine if we successfully bound to it,
382      * or revert to the previous engine if we didn't.
383      */
onUpdateEngine(int status)384     public void onUpdateEngine(int status) {
385         if (status == TextToSpeech.SUCCESS) {
386             if (DBG) {
387                 Log.d(TAG, "Updating engine: Successfully bound to the engine: " +
388                         mTts.getCurrentEngine());
389             }
390             checkVoiceData(mTts.getCurrentEngine());
391         } else {
392             if (DBG) Log.d(TAG, "Updating engine: Failed to bind to engine, reverting.");
393             if (mPreviousEngine != null) {
394                 // This is guaranteed to at least bind, since mPreviousEngine would be
395                 // null if the previous bind to this engine failed.
396                 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener,
397                         mPreviousEngine);
398             }
399             mPreviousEngine = null;
400         }
401     }
402 
403     /*
404      * Step 4: Check whether the voice data for the engine is ok.
405      */
checkVoiceData(String engine)406     private void checkVoiceData(String engine) {
407         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
408         intent.setPackage(engine);
409         try {
410             if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
411             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
412         } catch (ActivityNotFoundException ex) {
413             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
414         }
415     }
416 
417     /*
418      * Step 5: The voice data check is complete.
419      */
onVoiceDataIntegrityCheckDone(Intent data)420     private void onVoiceDataIntegrityCheckDone(Intent data) {
421         final String engine = mTts.getCurrentEngine();
422 
423         if (engine == null) {
424             Log.e(TAG, "Voice data check complete, but no engine bound");
425             return;
426         }
427 
428         if (data == null){
429             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
430                     mTts.getCurrentEngine());
431             return;
432         }
433 
434         Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine);
435 
436         final int engineCount = mEnginePreferenceCategory.getPreferenceCount();
437         for (int i = 0; i < engineCount; ++i) {
438             final Preference p = mEnginePreferenceCategory.getPreference(i);
439             if (p instanceof TtsEnginePreference) {
440                 TtsEnginePreference enginePref = (TtsEnginePreference) p;
441                 if (enginePref.getKey().equals(engine)) {
442                     enginePref.setVoiceDataDetails(data);
443                     break;
444                 }
445             }
446         }
447 
448         updateWidgetState(true);
449     }
450 
shouldDisplayDataAlert(String engine)451     private boolean shouldDisplayDataAlert(String engine) {
452         final EngineInfo info = mEnginesHelper.getEngineInfo(engine);
453         return !info.system;
454     }
455 
456     @Override
getCurrentChecked()457     public Checkable getCurrentChecked() {
458         return mCurrentChecked;
459     }
460 
461     @Override
getCurrentKey()462     public String getCurrentKey() {
463         return mCurrentEngine;
464     }
465 
466     @Override
setCurrentChecked(Checkable current)467     public void setCurrentChecked(Checkable current) {
468         mCurrentChecked = current;
469     }
470 
471     @Override
setCurrentKey(String key)472     public void setCurrentKey(String key) {
473         mCurrentEngine = key;
474         if (shouldDisplayDataAlert(mCurrentEngine)) {
475             displayDataAlert(mCurrentEngine);
476         } else {
477             updateDefaultEngine(mCurrentEngine);
478         }
479     }
480 
481 }
482