PageRenderTime 61ms CodeModel.GetById 31ms app.highlight 24ms RepoModel.GetById 2ms app.codeStats 0ms

/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
 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}