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