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