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