Home | History | Annotate | Download | only in tts
      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
    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
    144     public void onDestroy() {
    145         super.onDestroy();
    146         if (mTts != null) {
    147             mTts.shutdown();
    148             mTts = null;
    149         }
    150     }
    151 
    152     @Override
    153     public void onPause() {
    154         super.onPause();
    155         if ((mDefaultRatePref != null) && (mDefaultRatePref.getDialog() != null)) {
    156             mDefaultRatePref.getDialog().dismiss();
    157         }
    158     }
    159 
    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      */
    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      */
    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
    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 
    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 
    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 
    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      */
    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 
    323     private void updateWidgetState(boolean enable) {
    324         mPlayExample.setEnabled(enable);
    325         mDefaultRatePref.setEnabled(enable);
    326     }
    327 
    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 
    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      */
    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      */
    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      */
    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 
    451     private boolean shouldDisplayDataAlert(String engine) {
    452         final EngineInfo info = mEnginesHelper.getEngineInfo(engine);
    453         return !info.system;
    454     }
    455 
    456     @Override
    457     public Checkable getCurrentChecked() {
    458         return mCurrentChecked;
    459     }
    460 
    461     @Override
    462     public String getCurrentKey() {
    463         return mCurrentEngine;
    464     }
    465 
    466     @Override
    467     public void setCurrentChecked(Checkable current) {
    468         mCurrentChecked = current;
    469     }
    470 
    471     @Override
    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