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 17package com.android.settings.tts; 18 19import static android.provider.Settings.Secure.TTS_DEFAULT_RATE; 20import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; 21 22import com.android.settings.R; 23import com.android.settings.SettingsPreferenceFragment; 24import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState; 25 26import android.app.AlertDialog; 27import android.content.ActivityNotFoundException; 28import android.content.ContentResolver; 29import android.content.DialogInterface; 30import android.content.Intent; 31import android.os.Bundle; 32import android.preference.ListPreference; 33import android.preference.Preference; 34import android.preference.PreferenceActivity; 35import android.preference.PreferenceCategory; 36import android.provider.Settings; 37import android.provider.Settings.SettingNotFoundException; 38import android.speech.tts.TextToSpeech; 39import android.speech.tts.TextToSpeech.EngineInfo; 40import android.speech.tts.TtsEngines; 41import android.text.TextUtils; 42import android.util.Log; 43import android.widget.Checkable; 44 45import java.util.List; 46import java.util.Locale; 47 48public 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 private void maybeUpdateTtsLanguage(String currentEngine) { 196 if (currentEngine != null && mTts != null) { 197 final String localeString = mEnginesHelper.getLocalePrefForEngine( 198 currentEngine); 199 if (localeString != null) { 200 final String[] locale = TtsEngines.parseLocalePref(localeString); 201 final Locale newLocale = new Locale(locale[0], locale[1], locale[2]); 202 final Locale engineLocale = mTts.getLanguage(); 203 204 if (!newLocale.equals(engineLocale)) { 205 if (DBG) Log.d(TAG, "Loading language ahead of sample check : " + locale); 206 mTts.setLanguage(newLocale); 207 } 208 } 209 } 210 } 211 212 /** 213 * Ask the current default engine to return a string of sample text to be 214 * spoken to the user. 215 */ 216 private void getSampleText() { 217 String currentEngine = mTts.getCurrentEngine(); 218 219 if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine(); 220 221 maybeUpdateTtsLanguage(currentEngine); 222 Locale currentLocale = mTts.getLanguage(); 223 224 // TODO: This is currently a hidden private API. The intent extras 225 // and the intent action should be made public if we intend to make this 226 // a public API. We fall back to using a canned set of strings if this 227 // doesn't work. 228 Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT); 229 230 if (currentLocale != null) { 231 intent.putExtra("language", currentLocale.getLanguage()); 232 intent.putExtra("country", currentLocale.getCountry()); 233 intent.putExtra("variant", currentLocale.getVariant()); 234 } 235 intent.setPackage(currentEngine); 236 237 try { 238 if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0)); 239 startActivityForResult(intent, GET_SAMPLE_TEXT); 240 } catch (ActivityNotFoundException ex) { 241 Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")"); 242 } 243 } 244 245 /** 246 * Called when the TTS engine is initialized. 247 */ 248 public void onInitEngine(int status) { 249 if (status == TextToSpeech.SUCCESS) { 250 updateWidgetState(true); 251 if (DBG) Log.d(TAG, "TTS engine for settings screen initialized."); 252 } else { 253 if (DBG) Log.d(TAG, "TTS engine for settings screen failed to initialize successfully."); 254 updateWidgetState(false); 255 } 256 } 257 258 /** 259 * Called when voice data integrity check returns 260 */ 261 @Override 262 public void onActivityResult(int requestCode, int resultCode, Intent data) { 263 if (requestCode == GET_SAMPLE_TEXT) { 264 onSampleTextReceived(resultCode, data); 265 } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { 266 onVoiceDataIntegrityCheckDone(data); 267 } 268 } 269 270 private String getDefaultSampleString() { 271 if (mTts != null && mTts.getLanguage() != null) { 272 final String currentLang = mTts.getLanguage().getISO3Language(); 273 String[] strings = getActivity().getResources().getStringArray( 274 R.array.tts_demo_strings); 275 String[] langs = getActivity().getResources().getStringArray( 276 R.array.tts_demo_string_langs); 277 278 for (int i = 0; i < strings.length; ++i) { 279 if (langs[i].equals(currentLang)) { 280 return strings[i]; 281 } 282 } 283 } 284 return null; 285 } 286 287 private void onSampleTextReceived(int resultCode, Intent data) { 288 String sample = getDefaultSampleString(); 289 290 if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) { 291 if (data != null && data.getStringExtra("sampleText") != null) { 292 sample = data.getStringExtra("sampleText"); 293 } 294 if (DBG) Log.d(TAG, "Got sample text: " + sample); 295 } else { 296 if (DBG) Log.d(TAG, "Using default sample text :" + sample); 297 } 298 299 if (sample != null && mTts != null) { 300 // The engine is guaranteed to have been initialized here 301 // because this preference is not enabled otherwise. 302 mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, null); 303 } else { 304 // TODO: Display an error here to the user. 305 Log.e(TAG, "Did not have a sample string for the requested language"); 306 } 307 } 308 309 public boolean onPreferenceChange(Preference preference, Object objValue) { 310 if (KEY_DEFAULT_RATE.equals(preference.getKey())) { 311 // Default rate 312 mDefaultRate = Integer.parseInt((String) objValue); 313 try { 314 Settings.Secure.putInt(getContentResolver(), TTS_DEFAULT_RATE, mDefaultRate); 315 if (mTts != null) { 316 mTts.setSpeechRate(mDefaultRate / 100.0f); 317 } 318 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate); 319 } catch (NumberFormatException e) { 320 Log.e(TAG, "could not persist default TTS rate setting", e); 321 } 322 } 323 324 return true; 325 } 326 327 /** 328 * Called when mPlayExample is clicked 329 */ 330 public boolean onPreferenceClick(Preference preference) { 331 if (preference == mPlayExample) { 332 // Get the sample text from the TTS engine; onActivityResult will do 333 // the actual speaking 334 getSampleText(); 335 return true; 336 } 337 338 return false; 339 } 340 341 private void updateWidgetState(boolean enable) { 342 mPlayExample.setEnabled(enable); 343 mDefaultRatePref.setEnabled(enable); 344 } 345 346 private void displayDataAlert(final String key) { 347 Log.i(TAG, "Displaying data alert for :" + key); 348 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 349 builder.setTitle(android.R.string.dialog_alert_title); 350 builder.setIcon(android.R.drawable.ic_dialog_alert); 351 builder.setMessage(getActivity().getString( 352 R.string.tts_engine_security_warning, mEnginesHelper.getEngineInfo(key).label)); 353 builder.setCancelable(true); 354 builder.setPositiveButton(android.R.string.ok, 355 new DialogInterface.OnClickListener() { 356 public void onClick(DialogInterface dialog, int which) { 357 updateDefaultEngine(key); 358 } 359 }); 360 builder.setNegativeButton(android.R.string.cancel, null); 361 362 AlertDialog dialog = builder.create(); 363 dialog.show(); 364 } 365 366 private void updateDefaultEngine(String engine) { 367 if (DBG) Log.d(TAG, "Updating default synth to : " + engine); 368 369 // Disable the "play sample text" preference and the speech 370 // rate preference while the engine is being swapped. 371 updateWidgetState(false); 372 373 // Keep track of the previous engine that was being used. So that 374 // we can reuse the previous engine. 375 // 376 // Note that if TextToSpeech#getCurrentEngine is not null, it means at 377 // the very least that we successfully bound to the engine service. 378 mPreviousEngine = mTts.getCurrentEngine(); 379 380 // Step 1: Shut down the existing TTS engine. 381 if (mTts != null) { 382 try { 383 mTts.shutdown(); 384 mTts = null; 385 } catch (Exception e) { 386 Log.e(TAG, "Error shutting down TTS engine" + e); 387 } 388 } 389 390 // Step 2: Connect to the new TTS engine. 391 // Step 3 is continued on #onUpdateEngine (below) which is called when 392 // the app binds successfully to the engine. 393 if (DBG) Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine); 394 mTts = new TextToSpeech(getActivity().getApplicationContext(), mUpdateListener, engine); 395 } 396 397 /* 398 * Step 3: We have now bound to the TTS engine the user requested. We will 399 * attempt to check voice data for the engine if we successfully bound to it, 400 * or revert to the previous engine if we didn't. 401 */ 402 public void onUpdateEngine(int status) { 403 if (status == TextToSpeech.SUCCESS) { 404 if (DBG) { 405 Log.d(TAG, "Updating engine: Successfully bound to the engine: " + 406 mTts.getCurrentEngine()); 407 } 408 checkVoiceData(mTts.getCurrentEngine()); 409 } else { 410 if (DBG) Log.d(TAG, "Updating engine: Failed to bind to engine, reverting."); 411 if (mPreviousEngine != null) { 412 // This is guaranteed to at least bind, since mPreviousEngine would be 413 // null if the previous bind to this engine failed. 414 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener, 415 mPreviousEngine); 416 } 417 mPreviousEngine = null; 418 } 419 } 420 421 /* 422 * Step 4: Check whether the voice data for the engine is ok. 423 */ 424 private void checkVoiceData(String engine) { 425 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); 426 intent.setPackage(engine); 427 try { 428 if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0)); 429 startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); 430 } catch (ActivityNotFoundException ex) { 431 Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")"); 432 } 433 } 434 435 /* 436 * Step 5: The voice data check is complete. 437 */ 438 private void onVoiceDataIntegrityCheckDone(Intent data) { 439 final String engine = mTts.getCurrentEngine(); 440 441 if (engine == null) { 442 Log.e(TAG, "Voice data check complete, but no engine bound"); 443 return; 444 } 445 446 if (data == null){ 447 Log.e(TAG, "Engine failed voice data integrity check (null return)" + 448 mTts.getCurrentEngine()); 449 return; 450 } 451 452 Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine); 453 454 final int engineCount = mEnginePreferenceCategory.getPreferenceCount(); 455 for (int i = 0; i < engineCount; ++i) { 456 final Preference p = mEnginePreferenceCategory.getPreference(i); 457 if (p instanceof TtsEnginePreference) { 458 TtsEnginePreference enginePref = (TtsEnginePreference) p; 459 if (enginePref.getKey().equals(engine)) { 460 enginePref.setVoiceDataDetails(data); 461 break; 462 } 463 } 464 } 465 466 updateWidgetState(true); 467 } 468 469 private boolean shouldDisplayDataAlert(String engine) { 470 final EngineInfo info = mEnginesHelper.getEngineInfo(engine); 471 return !info.system; 472 } 473 474 @Override 475 public Checkable getCurrentChecked() { 476 return mCurrentChecked; 477 } 478 479 @Override 480 public String getCurrentKey() { 481 return mCurrentEngine; 482 } 483 484 @Override 485 public void setCurrentChecked(Checkable current) { 486 mCurrentChecked = current; 487 } 488 489 @Override 490 public void setCurrentKey(String key) { 491 mCurrentEngine = key; 492 if (shouldDisplayDataAlert(mCurrentEngine)) { 493 displayDataAlert(mCurrentEngine); 494 } else { 495 updateDefaultEngine(mCurrentEngine); 496 } 497 } 498 499} 500