/TalkBack/src/com/google/android/marvin/talkback/SpeechController.java

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