/TalkBack/src/com/google/android/marvin/talkback/SpeechController.java
Java | 449 lines | 237 code | 75 blank | 137 comment | 45 complexity | ae7fbee91795b853bad0a625ccb99e1b MD5 | raw file
1/* 2 * Copyright (C) 2011 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.google.android.marvin.talkback; 18 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.media.AudioManager; 24import android.os.Environment; 25import android.os.Handler; 26import android.speech.tts.TextToSpeech; 27import android.telephony.TelephonyManager; 28import android.text.TextUtils; 29import android.util.Log; 30 31import java.util.ArrayList; 32import java.util.HashMap; 33import java.util.Iterator; 34 35/** 36 * Handles text-to-speech. 37 * 38 * @author alanv@google.com (Alan Viverette) 39 */ 40class SpeechController { 41 /** Prefix for utterance IDs. */ 42 private static final String UTTERANCE_ID_PREFIX = "talkback_"; 43 44 /** Queuing mode - queue the utterance to be spoken. */ 45 public static final int QUEUING_MODE_QUEUE = TextToSpeech.QUEUE_ADD; 46 47 /** Queuing mode - interrupt the current utterance. */ 48 public static final int QUEUING_MODE_INTERRUPT = TextToSpeech.QUEUE_FLUSH; 49 50 /** Queuing mode - uninterruptible utterance. */ 51 public static final int QUEUING_MODE_UNINTERRUPTIBLE = 2; 52 53 /** 54 * Reusable map used for passing parameters to the TextToSpeech. 55 */ 56 private final HashMap<String, String> mSpeechParametersMap = new HashMap<String, String>(); 57 58 /** 59 * Array of actions to perform when an utterance completes. 60 */ 61 private final ArrayList<UtteranceCompleteAction> mUtteranceCompleteActions = 62 new ArrayList<UtteranceCompleteAction>(); 63 64 /** 65 * {@link BroadcastReceiver} for determining changes in the media state used 66 * for switching the TTS engine. 67 */ 68 private final MediaMountStateMonitor mMediaStateMonitor = new MediaMountStateMonitor(); 69 70 /** Handler used to return to the main thread from the TTS thread. */ 71 private final Handler mHandler = new Handler(); 72 73 /** The parent context. */ 74 private final Context mContext; 75 76 /** The audio manager, used to query ringer volume. */ 77 private final AudioManager mAudioManager; 78 79 /** The telephone manager, used to query ringer state. */ 80 private final TelephonyManager mTelephonyManager; 81 82 /** Handles connecting to BT headsets. */ 83 private BluetoothHandler mBluetoothHandler; 84 85 /** The package name of the default TTS engine. */ 86 private String mDefaultTtsEngine; 87 88 /** The TTS engine. */ 89 private TextToSpeech mTts; 90 91 /** A temporary TTS used for switching engines. */ 92 private TextToSpeech mTempTts; 93 94 /** Whether the TTS is currently speaking an uninterruptible utterance. */ 95 private boolean mUninterruptible; 96 97 /** 98 * The next utterance index; each utterance id will be constructed from this 99 * ever-increasing index. 100 */ 101 private int mNextUtteranceIndex = 0; 102 103 public SpeechController(Context context, boolean deviceIsPhone) { 104 mContext = context; 105 mContext.registerReceiver(mMediaStateMonitor, mMediaStateMonitor.getFilter()); 106 107 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 108 mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 109 110 mUninterruptible = false; 111 112 mTts = new TextToSpeech(context, mTtsInitListener); 113 } 114 115 /** 116 * Sets whether speech will be output through a Bluetooth headset when 117 * available. 118 * 119 * @param enabled {@code true} to output speech through a Bluetooth headset. 120 */ 121 public void setBluetoothEnabled(boolean enabled) { 122 if (enabled && mBluetoothHandler == null) { 123 mBluetoothHandler = new BluetoothHandler(5000, mBluetoothListener); 124 } else if (!enabled && mBluetoothHandler != null) { 125 mBluetoothHandler.stopSco(); 126 mBluetoothHandler.stop(); 127 mBluetoothHandler = null; 128 } 129 } 130 131 /** 132 * Cleans up and speaks an <code>utterance</code>. The clean up is replacing 133 * special strings with predefined mappings and reordering of some RegExp 134 * matches to improve presentation. The <code>queueMode</code> determines if 135 * speaking the event interrupts the speaking of previous events or is 136 * queued. 137 * 138 * @param text The text to speak. 139 * @param queueMode The queue mode to use for speaking. 140 */ 141 public void cleanUpAndSpeak(CharSequence text, int queueMode) { 142 if (TextUtils.isEmpty(text)) { 143 return; 144 } 145 146 // Reuse the global instance of speech parameters. 147 final HashMap<String, String> speechParams = mSpeechParametersMap; 148 speechParams.clear(); 149 150 // Give every utterance an unique identifier with an increasing index. 151 final String utteranceId = UTTERANCE_ID_PREFIX + mNextUtteranceIndex; 152 speechParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId); 153 154 if (isDeviceRinging()) { 155 manageRingerVolume(mNextUtteranceIndex); 156 } 157 158 if (queueMode == SpeechController.QUEUING_MODE_UNINTERRUPTIBLE) { 159 queueMode = TextToSpeech.QUEUE_FLUSH; 160 mUninterruptible = true; 161 addUtteranceCompleteAction(mNextUtteranceIndex, mClearUninterruptible); 162 } else if (queueMode == TextToSpeech.QUEUE_FLUSH && mUninterruptible) { 163 LogUtils.log(Log.DEBUG, "Can't interrupt right now, queueing instead."); 164 queueMode = TextToSpeech.QUEUE_ADD; 165 } 166 167 manageBluetoothConnection(speechParams); 168 169 mNextUtteranceIndex++; 170 171 LogUtils.log(TalkBackService.class, Log.VERBOSE, "Speaking with queue mode %d: \"%s\"", 172 queueMode, text); 173 174 mTts.speak(text.toString(), queueMode, speechParams); 175 } 176 177 /** 178 * @return {@code true} if the device is ringing. 179 */ 180 private boolean isDeviceRinging() { 181 return mTelephonyManager != null 182 && (mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_RINGING); 183 } 184 185 /** 186 * Handle speaking through mono Bluetooth headsets. 187 */ 188 private void manageBluetoothConnection(HashMap<String, String> speechParams) { 189 if (mBluetoothHandler == null || !mBluetoothHandler.isBluetoothAvailable()) { 190 // Can't output -- either disabled or no connection. 191 return; 192 } 193 194 if (!mBluetoothHandler.isAudioConnected()) { 195 mBluetoothHandler.start(mContext); 196 } 197 198 speechParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM, 199 Integer.toString(AudioManager.STREAM_VOICE_CALL)); 200 201 LogUtils.log(SpeechController.class, Log.DEBUG, "Connected to Bluetooth headset!"); 202 } 203 204 /** 205 * Add a new action that will be run when the given utterance index 206 * completes. 207 * 208 * @param utteranceIndex The index of the utterance that should finish 209 * before this action is executed. 210 * @param runnable The code to execute. 211 */ 212 public void addUtteranceCompleteAction(int utteranceIndex, Runnable runnable) { 213 synchronized (mUtteranceCompleteActions) { 214 final UtteranceCompleteAction action = 215 new UtteranceCompleteAction(utteranceIndex, runnable); 216 217 mUtteranceCompleteActions.add(action); 218 } 219 } 220 221 /** 222 * Method that's called by TTS whenever an utterance is completed. Do common 223 * tasks and execute any UtteranceCompleteActions associate with this 224 * utterance index (or an earlier index, in case one was accidentally 225 * dropped). 226 * 227 * @param utteranceId The utteranceId from the onUtteranceCompleted callback 228 * - we expect this to consist of UTTERANCE_ID_PREFIX followed by 229 * the utterance index. 230 */ 231 private void handleUtteranceCompleted(String utteranceId) { 232 if (!utteranceId.startsWith(UTTERANCE_ID_PREFIX)) { 233 return; 234 } 235 236 final int utteranceIndex; 237 238 try { 239 utteranceIndex = Integer.parseInt(utteranceId.substring(UTTERANCE_ID_PREFIX.length())); 240 } catch (NumberFormatException e) { 241 return; 242 } 243 244 synchronized (mUtteranceCompleteActions) { 245 final Iterator<UtteranceCompleteAction> iterator = mUtteranceCompleteActions.iterator(); 246 247 while (iterator.hasNext()) { 248 final UtteranceCompleteAction action = iterator.next(); 249 250 if (action.utteranceIndex <= utteranceIndex) { 251 mHandler.post(action.runnable); 252 iterator.remove(); 253 } 254 } 255 } 256 } 257 258 /** 259 * Decreases the ringer volume and registers a listener for the event of 260 * completing to speak which restores the volume to its previous level. 261 * 262 * @param utteranceIndex the index of this utterance, used to schedule an 263 * utterance completion action. 264 */ 265 private void manageRingerVolume(int utteranceIndex) { 266 final int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING); 267 final int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); 268 final int lowerEnoughVolume = Math.max((maxRingerVolume / 3), (currentRingerVolume / 2)); 269 270 mAudioManager.setStreamVolume(AudioManager.STREAM_RING, lowerEnoughVolume, 0); 271 272 addUtteranceCompleteAction(utteranceIndex, new Runnable() { 273 @Override 274 public void run() { 275 mAudioManager.setStreamVolume(AudioManager.STREAM_RING, currentRingerVolume, 0); 276 } 277 }); 278 } 279 280 /** 281 * Try to switch the TTS engine. 282 * 283 * @param engine The package name of the desired TTS engine 284 */ 285 private void setTtsEngine(String engine) { 286 mTempTts = new TextToSpeech(mContext, mTtsChangeListener, engine); 287 } 288 289 public void interrupt() { 290 mTts.stop(); 291 } 292 293 public void shutdown() { 294 mTts.shutdown(); 295 296 if (mMediaStateMonitor != null) { 297 mContext.unregisterReceiver(mMediaStateMonitor); 298 } 299 } 300 301 /** Clears the flag indicating that the TTS shouldn't be interrupted. */ 302 private final Runnable mClearUninterruptible = new Runnable() { 303 @Override 304 public void run() { 305 mUninterruptible = false; 306 } 307 }; 308 309 /** Hands off utterance completed processing. */ 310 private final TextToSpeech.OnUtteranceCompletedListener mUtteranceCompletedListener = 311 new TextToSpeech.OnUtteranceCompletedListener() { 312 @Override 313 public void onUtteranceCompleted(String utteranceId) { 314 handleUtteranceCompleted(utteranceId); 315 } 316 }; 317 318 /** 319 * When changing TTS engines, switches the active TTS engine when the new 320 * engine is initialized. 321 */ 322 private final TextToSpeech.OnInitListener mTtsChangeListener = 323 new TextToSpeech.OnInitListener() { 324 @Override 325 public void onInit(int status) { 326 if (status == TextToSpeech.SUCCESS) { 327 if (mTts != null) { 328 mTts.shutdown(); 329 } 330 331 mTts = mTempTts; 332 } 333 } 334 }; 335 336 /** 337 * After initialization has completed, sets up an 338 * OnUtteranceCompletedListener, registers earcons, and records the default 339 * TTS engine. 340 */ 341 private final TextToSpeech.OnInitListener mTtsInitListener = new TextToSpeech.OnInitListener() { 342 @Override 343 public void onInit(int status) { 344 if (status != TextToSpeech.SUCCESS) { 345 LogUtils.log(TalkBackService.class, Log.ERROR, "TTS init failed."); 346 return; 347 } 348 349 // TODO(alanv): There is a race condition whereby this can execute 350 // before mTts is set, resulting in a NPE. 351 mTts.setOnUtteranceCompletedListener(mUtteranceCompletedListener); 352 mDefaultTtsEngine = mTts.getDefaultEngine(); 353 354 final String mediaState = Environment.getExternalStorageState(); 355 356 if (!mediaState.equals(Environment.MEDIA_MOUNTED) 357 && !mediaState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { 358 setTtsEngine("com.google.android.tts"); 359 } 360 } 361 }; 362 363 private final BluetoothHandler.Listener mBluetoothListener = new BluetoothHandler.Listener() { 364 @Override 365 public void onConnectionComplete() { 366 final BluetoothHandler bluetoothHandler = mBluetoothHandler; 367 368 if (bluetoothHandler != null) { 369 mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 370 mBluetoothHandler.startSco(); 371 } 372 } 373 }; 374 375 /** 376 * {@link BroadcastReceiver} for detecting media mount and unmount. 377 */ 378 private class MediaMountStateMonitor extends BroadcastReceiver { 379 private final IntentFilter mMediaIntentFilter; 380 381 public MediaMountStateMonitor() { 382 mMediaIntentFilter = new IntentFilter(); 383 mMediaIntentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED); 384 mMediaIntentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 385 mMediaIntentFilter.addDataScheme("file"); 386 } 387 388 public IntentFilter getFilter() { 389 return mMediaIntentFilter; 390 } 391 392 @Override 393 public void onReceive(Context context, Intent intent) { 394 final String action = intent.getAction(); 395 396 // If the SD card is unmounted, switch to the default TTS engine. 397 // TODO(alanv): Shouldn't the TTS service handle this automatically? 398 if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) { 399 if (mDefaultTtsEngine != null) { 400 setTtsEngine(mDefaultTtsEngine); 401 } 402 } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 403 setTtsEngine("com.google.android.tts"); 404 } 405 } 406 } 407 408 /** 409 * An action that should be performed after a particular utterance index 410 * completes. 411 */ 412 private static class UtteranceCompleteAction { 413 public UtteranceCompleteAction(int utteranceIndex, Runnable runnable) { 414 this.utteranceIndex = utteranceIndex; 415 this.runnable = runnable; 416 } 417 418 /** 419 * The minimum utterance index that must complete before this action 420 * should be performed. 421 */ 422 public int utteranceIndex; 423 424 /** 425 * The action to execute. 426 */ 427 public Runnable runnable; 428 } 429 430 /** 431 * Removes all instance of the specified runnable from the utterance 432 * complete action list. 433 * 434 * @param runnable The runnable to remove. 435 */ 436 public void removeUtteranceCompleteAction(Runnable runnable) { 437 synchronized (mUtteranceCompleteActions) { 438 final Iterator<UtteranceCompleteAction> i = mUtteranceCompleteActions.iterator(); 439 440 while (i.hasNext()) { 441 final UtteranceCompleteAction action = i.next(); 442 443 if (action.runnable == runnable) { 444 i.remove(); 445 } 446 } 447 } 448 } 449}