Home | History | Annotate | Download | only in voicedialer
      1 /*
      2  * Copyright (C) 2010 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.voicedialer;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.bluetooth.BluetoothHeadset;
     22 import android.content.BroadcastReceiver;
     23 import android.content.Context;
     24 import android.content.DialogInterface;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.media.AudioManager;
     28 import android.media.ToneGenerator;
     29 import android.os.Bundle;
     30 import android.os.Environment;
     31 import android.os.Handler;
     32 import android.os.SystemProperties;
     33 import android.os.Vibrator;
     34 import android.speech.tts.TextToSpeech;
     35 import android.util.Config;
     36 import android.util.Log;
     37 import android.view.View;
     38 import android.view.WindowManager;
     39 import android.widget.TextView;
     40 import java.io.File;
     41 import java.io.InputStream;
     42 import java.util.HashMap;
     43 
     44 /**
     45  * TODO: get rid of the anonymous classes
     46  *
     47  * This class is the user interface of the BluetoothVoiceDialer application.
     48  * It begins in the INITIALIZING state.
     49  *
     50  * INITIALIZING :
     51  *  This transitions out on events from TTS and the BluetoothHeadset
     52  *   once TTS initialized and SCO channel set up:
     53  *     * prompt the user "speak now"
     54  *     * transition to the SPEAKING_GREETING state
     55  *
     56  * SPEAKING_GREETING:
     57  *  This transitions out only on events from TTS or the fallback runnable
     58  *   once the greeting utterance completes:
     59  *     * begin listening for the command using the {@link CommandRecognizerEngine}
     60  *     * transition to the WAITING_FOR_COMMAND state
     61  *
     62  * WAITING_FOR_COMMAND :
     63  * This transitions out only on events from the recognizer
     64  *   on RecognitionFailure or RecognitionError:
     65  *     * begin speaking "try again."
     66  *     * remain in state SPEAKING_TRY_AGAIN
     67  *   on RecognitionSuccess:
     68  *     single result:
     69  *       * begin speaking the sentence describing the intent
     70  *       * transition to the SPEAKING_CHOSEN_ACTION
     71  *     multiple results:
     72  *       * begin speaking each of the choices in order
     73  *       * transition to the SPEAKING_CHOICES state
     74  *
     75  * SPEAKING_TRY_AGAIN:
     76  * This transitions out only on events from TTS or the fallback runnable
     77  *   once the try again utterance completes:
     78  *     * begin listening for the command using the {@link CommandRecognizerEngine}
     79  *     * transition to the LISTENING_FOR_COMMAND state
     80  *
     81  * SPEAKING_CHOSEN_ACTION:
     82  *  This transitions out only on events from TTS or the fallback runnable
     83  *   once the utterance completes:
     84  *     * dispatch the intent that was chosen
     85  *     * transition to the EXITING state
     86  *     * finish the activity
     87  *
     88  * SPEAKING_CHOICES:
     89  *  This transitions out only on events from TTS or the fallback runnable
     90  *   once the utterance completes:
     91  *     * begin listening for the user's choice using the
     92  *         {@link PhoneTypeChoiceRecognizerEngine}
     93  *     * transition to the WAITING_FOR_CHOICE state.
     94  *
     95  * WAITING_FOR_CHOICE:
     96  *  This transitions out only on events from the recognizer
     97  *   on RecognitionFailure or RecognitionError:
     98  *     * begin speaking the "invalid choice" message, along with the list
     99  *       of choices
    100  *     * transition to the SPEAKING_CHOICES state
    101  *   on RecognitionSuccess:
    102  *     if the result is "try again", prompt the user to say a command, begin
    103  *       listening for the command, and transition back to the WAITING_FOR_COMMAND
    104  *       state.
    105  *     if the result is "exit", then being speaking the "goodbye" message and
    106  *       transition to the SPEAKING_GOODBYE state.
    107  *     if the result is a valid choice, begin speaking the action chosen,initiate
    108  *       the command the user has choose and exit.
    109  *     if not a valid choice, speak the "invalid choice" message, begin
    110  *       speaking the choices in order again, transition to the
    111  *       SPEAKING_CHOICES
    112  *
    113  * SPEAKING_GOODBYE:
    114  *  This transitions out only on events from TTS or the fallback runnable
    115  *   after a time out, finish the activity.
    116  *
    117  */
    118 
    119 public class BluetoothVoiceDialerActivity extends Activity {
    120 
    121     private static final String TAG = "VoiceDialerActivity";
    122 
    123     private static final String MICROPHONE_EXTRA = "microphone";
    124     private static final String CONTACTS_EXTRA = "contacts";
    125 
    126     private static final String SPEAK_NOW_UTTERANCE = "speak_now";
    127     private static final String TRY_AGAIN_UTTERANCE = "try_again";
    128     private static final String CHOSEN_ACTION_UTTERANCE = "chose_action";
    129     private static final String GOODBYE_UTTERANCE = "goodbye";
    130     private static final String CHOICES_UTTERANCE = "choices";
    131 
    132     private static final int FIRST_UTTERANCE_DELAY = 300;
    133     private static final int MAX_TTS_DELAY = 6000;
    134 
    135     private static final int SAMPLE_RATE = 8000;
    136 
    137     private static final int INITIALIZING = 0;
    138     private static final int SPEAKING_GREETING = 1;
    139     private static final int WAITING_FOR_COMMAND = 2;
    140     private static final int SPEAKING_TRY_AGAIN = 3;
    141     private static final int SPEAKING_CHOICES = 4;
    142     private static final int WAITING_FOR_CHOICE = 5;
    143     private static final int SPEAKING_CHOSEN_ACTION = 6;
    144     private static final int SPEAKING_GOODBYE = 7;
    145     private static final int EXITING = 8;
    146 
    147     private static final CommandRecognizerEngine mCommandEngine =
    148             new CommandRecognizerEngine();
    149     private static final PhoneTypeChoiceRecognizerEngine mPhoneTypeChoiceEngine =
    150             new PhoneTypeChoiceRecognizerEngine();
    151     private CommandRecognizerClient mCommandClient;
    152     private ChoiceRecognizerClient mChoiceClient;
    153     private ToneGenerator mToneGenerator;
    154     private Handler mHandler;
    155     private Thread mRecognizerThread = null;
    156     private AudioManager mAudioManager;
    157     private BluetoothHeadset mBluetoothHeadset;
    158     private TextToSpeech mTts;
    159     private HashMap<String, String> mTtsParams;
    160     private VoiceDialerBroadcastReceiver mReceiver;
    161     private int mBluetoothAudioState;
    162     private boolean mWaitingForTts;
    163     private boolean mWaitingForScoConnection;
    164     private Intent[] mAvailableChoices;
    165     private Intent mChosenAction;
    166     private int mBluetoothVoiceVolume;
    167     private int mState;
    168     private AlertDialog mAlertDialog;
    169     private Runnable mFallbackRunnable;
    170 
    171     @Override
    172     protected void onCreate(Bundle icicle) {
    173         if (Config.LOGD) Log.d(TAG, "onCreate");
    174         super.onCreate(icicle);
    175         mHandler = new Handler();
    176         mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
    177         mToneGenerator = new ToneGenerator(AudioManager.STREAM_RING,
    178                 ToneGenerator.MAX_VOLUME);
    179     }
    180 
    181     protected void onStart() {
    182         if (Config.LOGD) Log.d(TAG, "onStart " + getIntent());
    183         super.onStart();
    184 
    185         mState = INITIALIZING;
    186         mChosenAction = null;
    187         mAudioManager.requestAudioFocus(
    188                 null, AudioManager.STREAM_MUSIC,
    189                 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
    190 
    191         // set this flag so this activity will stay in front of the keyguard
    192         int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
    193         getWindow().addFlags(flags);
    194 
    195         // open main window
    196         setTheme(android.R.style.Theme_Dialog);
    197         setTitle(R.string.bluetooth_title);
    198         setContentView(R.layout.voice_dialing);
    199         findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
    200         findViewById(R.id.retry_view).setVisibility(View.INVISIBLE);
    201         findViewById(R.id.microphone_loading_view).setVisibility(View.VISIBLE);
    202         if (RecognizerLogger.isEnabled(this)) {
    203             ((TextView) findViewById(R.id.substate)).setText(R.string.logging_enabled);
    204         }
    205 
    206         // Get handle to BluetoothHeadset object
    207         IntentFilter audioStateFilter;
    208         audioStateFilter = new IntentFilter();
    209         audioStateFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
    210         mReceiver = new VoiceDialerBroadcastReceiver();
    211         registerReceiver(mReceiver, audioStateFilter);
    212 
    213         mCommandEngine.setContactsFile(newFile(getArg(CONTACTS_EXTRA)));
    214         mCommandEngine.setMinimizeResults(true);
    215         mCommandEngine.setAllowOpenEntries(false);
    216         mCommandClient = new CommandRecognizerClient();
    217         mChoiceClient = new ChoiceRecognizerClient();
    218 
    219         mBluetoothAudioState = BluetoothHeadset.STATE_ERROR;
    220 
    221         if (BluetoothHeadset.isBluetoothVoiceDialingEnabled(this)) {
    222             // we can't start recognizing until we get connected to the BluetoothHeadset
    223             // and have an connected audio state.  We will listen for these
    224             // states to change.
    225             mWaitingForScoConnection = true;
    226             mBluetoothHeadset = new BluetoothHeadset(this,
    227                     mBluetoothHeadsetServiceListener);
    228             // initialize the text to speech system
    229             mWaitingForTts = true;
    230             mTts = new TextToSpeech(this, new TtsInitListener());
    231             mTtsParams = new HashMap<String, String>();
    232             mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
    233                     String.valueOf(AudioManager.STREAM_BLUETOOTH_SCO));
    234         } else {
    235             // bluetooth voice dialing is disabled, just exit
    236             finish();
    237         }
    238     }
    239 
    240     class ErrorRunnable implements Runnable {
    241         private int mErrorMsg;
    242         public ErrorRunnable(int errorMsg) {
    243             mErrorMsg = errorMsg;
    244         }
    245 
    246         public void run() {
    247             // put up an error and exit
    248             mHandler.removeCallbacks(mMicFlasher);
    249             ((TextView)findViewById(R.id.state)).setText(R.string.failure);
    250             ((TextView)findViewById(R.id.substate)).setText(mErrorMsg);
    251             ((TextView)findViewById(R.id.substate)).setText(
    252                     R.string.headset_connection_lost);
    253             findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
    254             findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
    255 
    256             playSound(ToneGenerator.TONE_PROP_NACK);
    257         }
    258     }
    259 
    260     class FallbackRunnable implements Runnable {
    261         public void run() {
    262             Log.e(TAG, "utterance completion not delivered, using fallback");
    263             // This runnable is intended as a fallback to transition to
    264             // the next state is for some reason we never get a
    265             // TTS utterance completion.  It will behave just the same
    266             // as if we had received utterance completion.
    267             onSpeechCompletion();
    268         }
    269     }
    270 
    271     class GreetingRunnable implements Runnable {
    272         public void run() {
    273             mState = SPEAKING_GREETING;
    274             mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    275                     SPEAK_NOW_UTTERANCE);
    276             mTts.speak(getString(R.string.speak_now_tts),
    277                 TextToSpeech.QUEUE_FLUSH,
    278                 mTtsParams);
    279             // Normally, the we will begin listening for the command after the
    280             // utterance completes.  As a fallback in case the utterance
    281             // does not complete, post a delayed runnable to fire
    282             // the intent.
    283             mFallbackRunnable = new FallbackRunnable();
    284             mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    285         }
    286     }
    287 
    288     class TtsInitListener implements TextToSpeech.OnInitListener {
    289         public void onInit(int status) {
    290             // status can be either TextToSpeech.SUCCESS or TextToSpeech.ERROR.
    291             if (Config.LOGD) Log.d(TAG, "onInit for tts");
    292             if (status != TextToSpeech.SUCCESS) {
    293                 // Initialization failed.
    294                 Log.e(TAG, "Could not initialize TextToSpeech.");
    295                 mHandler.post(new ErrorRunnable(R.string.recognition_error));
    296                 exitActivity();
    297                 return;
    298             }
    299 
    300             if (mTts == null) {
    301                 Log.e(TAG, "null tts");
    302                 mHandler.post(new ErrorRunnable(R.string.recognition_error));
    303                 exitActivity();
    304                 return;
    305             }
    306 
    307             // The TTS engine has been successfully initialized.
    308             mWaitingForTts = false;
    309 
    310             mTts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener());
    311             // TTS over bluetooth is really loud,
    312             // store the current volume away, and then turn it down.
    313             // we will restore it in onStop.
    314             // Limit volume to -18dB. Stream volume range represents approximately 50dB
    315             // (See AudioSystem.cpp linearToLog()) so the number of steps corresponding
    316             // to 18dB is 18 / (50 / maxSteps).
    317             mBluetoothVoiceVolume = mAudioManager.getStreamVolume(
    318                     AudioManager.STREAM_BLUETOOTH_SCO);
    319             int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_BLUETOOTH_SCO);
    320             int volume = maxVolume - ((18 / (50/maxVolume)) + 1);
    321             if (mBluetoothVoiceVolume > volume) {
    322                 mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, volume, 0);
    323             }
    324 
    325             if (mWaitingForScoConnection) {
    326                 // the bluetooth connection is not up yet, still waiting.
    327             } else {
    328                 // we now have SCO connection and TTS, so we can start.
    329                 mHandler.postDelayed(new GreetingRunnable(), FIRST_UTTERANCE_DELAY);
    330             }
    331         }
    332     }
    333 
    334     class OnUtteranceCompletedListener
    335             implements TextToSpeech.OnUtteranceCompletedListener {
    336         public void onUtteranceCompleted(String utteranceId) {
    337             Log.d(TAG, "onUtteranceCompleted " + utteranceId);
    338             // since the utterance has completed, we no longer need the fallback.
    339             mHandler.removeCallbacks(mFallbackRunnable);
    340             mFallbackRunnable = null;
    341             mHandler.post(new Runnable() {
    342                 public void run() {
    343                     onSpeechCompletion();
    344                 }
    345             });
    346         }
    347     }
    348 
    349     private void onSpeechCompletion() {
    350         if (mState == SPEAKING_GREETING || mState == SPEAKING_TRY_AGAIN) {
    351             listenForCommand();
    352         } else if (mState == SPEAKING_CHOICES) {
    353             listenForChoice();
    354         } else if (mState == SPEAKING_GOODBYE) {
    355             mState = EXITING;
    356             finish();
    357         } else if (mState == SPEAKING_CHOSEN_ACTION) {
    358             mState = EXITING;
    359             startActivityHelp(mChosenAction);
    360             finish();
    361         }
    362     }
    363 
    364     private BluetoothHeadset.ServiceListener mBluetoothHeadsetServiceListener =
    365             new BluetoothHeadset.ServiceListener() {
    366         public void onServiceConnected() {
    367             if (mBluetoothHeadset != null &&
    368                     mBluetoothHeadset.getState() == BluetoothHeadset.STATE_CONNECTED) {
    369                 mBluetoothHeadset.startVoiceRecognition();
    370             }
    371 
    372             if (Config.LOGD) Log.d(TAG, "onServiceConnected");
    373         }
    374         public void onServiceDisconnected() {}
    375     };
    376 
    377     private class VoiceDialerBroadcastReceiver extends BroadcastReceiver {
    378         @Override
    379         public void onReceive(Context context, Intent intent) {
    380             String action = intent.getAction();
    381             if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
    382                 mBluetoothAudioState = intent.getIntExtra(
    383                         BluetoothHeadset.EXTRA_AUDIO_STATE,
    384                         BluetoothHeadset.STATE_ERROR);
    385                 if (Config.LOGD) Log.d(TAG, "HEADSET AUDIO_STATE_CHANGED -> " +
    386                         mBluetoothAudioState);
    387 
    388                 if (mBluetoothAudioState == BluetoothHeadset.AUDIO_STATE_CONNECTED &&
    389                     mWaitingForScoConnection) {
    390                     // SCO channel has just become available.
    391                     mWaitingForScoConnection = false;
    392                     if (mWaitingForTts) {
    393                         // still waiting for the TTS to be set up.
    394                     } else {
    395                         // we now have SCO connection and TTS, so we can start.
    396                         mHandler.postDelayed(new GreetingRunnable(), FIRST_UTTERANCE_DELAY);
    397                     }
    398                 } else {
    399                     if (!mWaitingForScoConnection) {
    400                         // apparently our connection to the headset has dropped.
    401                         // we won't be able to continue voicedialing.
    402                         if (Config.LOGD) Log.d(TAG, "lost sco connection");
    403 
    404                         mHandler.post(new ErrorRunnable(
    405                                 R.string.headset_connection_lost));
    406 
    407                         exitActivity();
    408                     }
    409                 }
    410             }
    411         }
    412     }
    413 
    414     private class CommandRecognizerClient implements RecognizerClient {
    415         /**
    416          * Called by the {@link RecognizerEngine} when the microphone is started.
    417          */
    418         public void onMicrophoneStart(InputStream mic) {
    419             if (Config.LOGD) Log.d(TAG, "onMicrophoneStart");
    420 
    421             mHandler.post(new Runnable() {
    422                 public void run() {
    423                     findViewById(R.id.retry_view).setVisibility(View.INVISIBLE);
    424                     findViewById(R.id.microphone_loading_view).setVisibility(
    425                             View.INVISIBLE);
    426                     ((TextView)findViewById(R.id.state)).setText(R.string.listening);
    427                     mHandler.post(mMicFlasher);
    428                 }
    429             });
    430         }
    431 
    432         /**
    433          * Called by the {@link RecognizerEngine} if the recognizer fails.
    434          */
    435         public void onRecognitionFailure(final String msg) {
    436             if (Config.LOGD) Log.d(TAG, "onRecognitionFailure " + msg);
    437             // we had zero results.  Just try again.
    438             askToTryAgain();
    439         }
    440 
    441         /**
    442          * Called by the {@link RecognizerEngine} on an internal error.
    443          */
    444         public void onRecognitionError(final String msg) {
    445             if (Config.LOGD) Log.d(TAG, "onRecognitionError " + msg);
    446             mHandler.post(new ErrorRunnable(R.string.recognition_error));
    447             exitActivity();
    448         }
    449 
    450         private void askToTryAgain() {
    451             // get work off UAPI thread
    452             mHandler.post(new Runnable() {
    453                 public void run() {
    454                     if (mAlertDialog != null) {
    455                         mAlertDialog.dismiss();
    456                     }
    457 
    458                     mState = SPEAKING_TRY_AGAIN;
    459                     mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    460                             TRY_AGAIN_UTTERANCE);
    461                     mTts.speak(getString(R.string.no_results_tts),
    462                         TextToSpeech.QUEUE_FLUSH,
    463                         mTtsParams);
    464 
    465                     mHandler.removeCallbacks(mMicFlasher);
    466                     ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again);
    467                     findViewById(R.id.state).setVisibility(View.VISIBLE);
    468                     findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
    469                     findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
    470 
    471                     // don't listen for command yet, wait for the utterance to complete.
    472                 }
    473             });
    474         }
    475 
    476         /**
    477          * Called by the {@link RecognizerEngine} when is succeeds.  If there is
    478          * only one item, then the Intent is dispatched immediately.
    479          * If there are more, then an AlertDialog is displayed and the user is
    480          * prompted to select.
    481          * @param intents a list of Intents corresponding to the sentences.
    482          */
    483         public void onRecognitionSuccess(final Intent[] intents) {
    484             if (Config.LOGD) Log.d(TAG, "onRecognitionSuccess " + intents.length);
    485 
    486             // store the intents in a member variable so that we can access it
    487             // later when the user choses which action to perform.
    488             mAvailableChoices = intents;
    489 
    490             mHandler.post(new Runnable() {
    491                 public void run() {
    492                     mHandler.removeCallbacks(mMicFlasher);
    493 
    494                     String[] sentences = new String[intents.length];
    495                     for (int i = 0; i < intents.length; i++) {
    496                         sentences[i] = intents[i].getStringExtra(
    497                                 RecognizerEngine.SENTENCE_EXTRA);
    498                     }
    499 
    500                     if (intents.length == 0) {
    501                         onRecognitionFailure("zero intents");
    502                         return;
    503                     }
    504 
    505                     if (intents.length > 0) {
    506                         // see if we the response was "exit" or "cancel".
    507                         String value = intents[0].getStringExtra(
    508                             RecognizerEngine.SEMANTIC_EXTRA);
    509                         if (Config.LOGD) Log.d(TAG, "value " + value);
    510                         if ("X".equals(value)) {
    511                             exitActivity();
    512                             return;
    513                         }
    514                     }
    515 
    516                     if ((intents.length == 1) ||
    517                             (!Intent.ACTION_CALL_PRIVILEGED.equals(
    518                                     intents[0].getAction()))) {
    519                         // Either there is only one match, or multiple
    520                         // matches for some type of intent other than "call".
    521                         // If there's only one match, we may as well just
    522                         // dispatch it.  If it's not a "call" intent, then
    523                         // we don't have a good way to let the user choose
    524                         // which match without touching the screen.  In this
    525                         // case, we simply take the highest confidence match.
    526 
    527                         // Speak the sentence for the action we are about
    528                         // to dispatch so that the user knows what is happening.
    529                         String sentenceSpoken = spaceOutDigits(
    530                                 mAvailableChoices[0].getStringExtra(
    531                                     RecognizerEngine.SENTENCE_EXTRA));
    532 
    533                         mState = SPEAKING_CHOSEN_ACTION;
    534                         mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    535                                 CHOSEN_ACTION_UTTERANCE);
    536                         mTts.speak(sentenceSpoken,
    537                             TextToSpeech.QUEUE_FLUSH,
    538                             mTtsParams);
    539                         mChosenAction = intents[0];
    540 
    541                         // Normally, the intent will be dispatched after the
    542                         // utterance completes.  As a fallback in case the utterance
    543                         // does not complete, post a delayed runnable to fire
    544                         // the intent.
    545                         mFallbackRunnable = new FallbackRunnable();
    546                         mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    547 
    548                         return;
    549                     } else {
    550                         // We have multiple call intents.  There should only
    551                         // be results for a single name, but multiple phone types.
    552                         // speak the choices to the user, and then listen for
    553                         // the choice.
    554                         // We will not start listening until the utterance
    555                         // of the choice list completes.
    556                         speakChoices();
    557 
    558                         // Normally, listening will begin after the
    559                         // utterance completes.  As a fallback in case the utterance
    560                         // does not complete, post a delayed runnable to begin
    561                         // listening.
    562                         mFallbackRunnable = new FallbackRunnable();
    563                         mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    564 
    565                         DialogInterface.OnCancelListener cancelListener =
    566                             new DialogInterface.OnCancelListener() {
    567 
    568                             public void onCancel(DialogInterface dialog) {
    569                                 if (Config.LOGD) {
    570                                     Log.d(TAG, "cancelListener.onCancel");
    571                                 }
    572                                 dialog.dismiss();
    573                                 finish();
    574                             }
    575                        };
    576 
    577                         DialogInterface.OnClickListener clickListener =
    578                             new DialogInterface.OnClickListener() {
    579 
    580                             public void onClick(DialogInterface dialog, int which) {
    581                                 if (Config.LOGD) {
    582                                     Log.d(TAG, "clickListener.onClick " + which);
    583                                 }
    584                                 startActivityHelp(intents[which]);
    585                                 dialog.dismiss();
    586                                 finish();
    587                             }
    588                         };
    589 
    590                         DialogInterface.OnClickListener negativeListener =
    591                             new DialogInterface.OnClickListener() {
    592 
    593                             public void onClick(DialogInterface dialog, int which) {
    594                                 if (Config.LOGD) {
    595                                     Log.d(TAG, "negativeListener.onClick " +
    596                                         which);
    597                                 }
    598                                 dialog.dismiss();
    599                                 finish();
    600                             }
    601                         };
    602 
    603                         mAlertDialog =
    604                                 new AlertDialog.Builder(
    605                                         BluetoothVoiceDialerActivity.this)
    606                                 .setTitle(R.string.title)
    607                                 .setItems(sentences, clickListener)
    608                                 .setOnCancelListener(cancelListener)
    609                                 .setNegativeButton(android.R.string.cancel,
    610                                         negativeListener)
    611                                 .show();
    612                     }
    613                 }
    614 
    615             });
    616         }
    617     }
    618 
    619     private class ChoiceRecognizerClient implements RecognizerClient {
    620         public void onRecognitionSuccess(final Intent[] intents) {
    621             if (Config.LOGD) Log.d(TAG, "ChoiceRecognizerClient onRecognitionSuccess");
    622 
    623             if (mAlertDialog != null) {
    624                 mAlertDialog.dismiss();
    625             }
    626 
    627             // disregard all but the first intent.
    628             if (intents.length > 0) {
    629                 String value = intents[0].getStringExtra(
    630                     RecognizerEngine.SEMANTIC_EXTRA);
    631                 if (Config.LOGD) Log.d(TAG, "value " + value);
    632                 if ("R".equals(value)) {
    633                     mHandler.post(new GreetingRunnable());
    634                 } else if ("X".equals(value)) {
    635                     exitActivity();
    636                 } else {
    637                     // it's a phone type response
    638                     mChosenAction = null;
    639                     for (int i = 0; i < mAvailableChoices.length; i++) {
    640                         if (value.equalsIgnoreCase(
    641                                 mAvailableChoices[i].getStringExtra(
    642                                         CommandRecognizerEngine.PHONE_TYPE_EXTRA))) {
    643                             mChosenAction = mAvailableChoices[i];
    644                         }
    645                     }
    646 
    647                     if (mChosenAction != null) {
    648                         mState = SPEAKING_CHOSEN_ACTION;
    649                         mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    650                                 CHOSEN_ACTION_UTTERANCE);
    651                         mTts.speak(mChosenAction.getStringExtra(
    652                                 RecognizerEngine.SENTENCE_EXTRA),
    653                             TextToSpeech.QUEUE_FLUSH,
    654                             mTtsParams);
    655 
    656                         // Normally, the intent will be dispatched after the
    657                         // utterance completes.  As a fallback in case the utterance
    658                         // does not complete, post a delayed runnable to fire
    659                         // the intent.
    660                         mFallbackRunnable = new FallbackRunnable();
    661                         mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    662                     } else {
    663                         // invalid choice
    664                         if (Config.LOGD) Log.d(TAG, "invalid choice" + value);
    665 
    666                         mTtsParams.remove(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID);
    667                         mTts.speak(getString(R.string.invalid_choice_tts),
    668                             TextToSpeech.QUEUE_FLUSH,
    669                             mTtsParams);
    670 
    671                         // repeat the list of choices.  We will not start
    672                         // listening until this utterance completes.
    673                         speakChoices();
    674 
    675                         // Normally, listening will begin after the
    676                         // utterance completes.  As a fallback in case the utterance
    677                         // does not complete, post a delayed runnable begin
    678                         // listening.
    679                         mFallbackRunnable = new FallbackRunnable();
    680                         mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    681                     }
    682                 }
    683             }
    684         }
    685 
    686         public void onRecognitionFailure(String msg) {
    687             if (Config.LOGD) Log.d(TAG, "ChoiceRecognizerClient onRecognitionFailure");
    688             exitActivity();
    689         }
    690 
    691         public void onRecognitionError(String err) {
    692             if (Config.LOGD) Log.d(TAG, "ChoiceRecognizerClient onRecognitionError");
    693             mHandler.post(new ErrorRunnable(R.string.recognition_error));
    694             exitActivity();
    695         }
    696 
    697         public void onMicrophoneStart(InputStream mic) {
    698             if (Config.LOGD) Log.d(TAG, "ChoiceRecognizerClient onMicrophoneStart");
    699         }
    700     }
    701 
    702     private void speakChoices() {
    703         if (Config.LOGD) Log.d(TAG, "speakChoices");
    704         mState = SPEAKING_CHOICES;
    705 
    706         String sentenceSpoken = spaceOutDigits(
    707                 mAvailableChoices[0].getStringExtra(
    708                     RecognizerEngine.SENTENCE_EXTRA));
    709 
    710         // When we have multiple choices, they will be of the form
    711         // "call jack jones at home", "call jack jones on mobile".
    712         // Speak the entire first sentence, then the last word from each
    713         // of the remaining sentences.  This will come out to something
    714         // like "call jack jones at home mobile or work".
    715         StringBuilder builder = new StringBuilder();
    716         builder.append(sentenceSpoken);
    717 
    718         int count = mAvailableChoices.length;
    719         for (int i=1; i < count; i++) {
    720             if (i == count-1) {
    721                 builder.append(" or ");
    722             } else {
    723                 builder.append(" ");
    724             }
    725             String tmpSentence = mAvailableChoices[i].getStringExtra(
    726                     RecognizerEngine.SENTENCE_EXTRA);
    727             String[] words = tmpSentence.trim().split(" ");
    728             builder.append(words[words.length-1]);
    729         }
    730         mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    731                 CHOICES_UTTERANCE);
    732         mTts.speak(builder.toString(),
    733             TextToSpeech.QUEUE_ADD,
    734             mTtsParams);
    735     }
    736 
    737 
    738     private static String spaceOutDigits(String sentenceDisplay) {
    739         // if we have a sentence of the form "dial 123 456 7890",
    740         // we need to insert a space between each digit, otherwise
    741         // the TTS engine will say "dial one hundred twenty three...."
    742         // When there already is a space, we also insert a comma,
    743         // so that it pauses between sections.  For the displayable
    744         // sentence "dial 123 456 7890" it will speak
    745         // "dial 1 2 3, 4 5 6, 7 8 9 0"
    746         char buffer[] = sentenceDisplay.toCharArray();
    747         StringBuilder builder = new StringBuilder();
    748         boolean buildingNumber = false;
    749         int l = sentenceDisplay.length();
    750         for (int index = 0; index < l; index++) {
    751             char c = buffer[index];
    752             if (Character.isDigit(c)) {
    753                 if (buildingNumber) {
    754                     builder.append(" ");
    755                 }
    756                 buildingNumber = true;
    757                 builder.append(c);
    758             } else if (c == ' ') {
    759                 if (buildingNumber) {
    760                     builder.append(",");
    761                 } else {
    762                     builder.append(" ");
    763                 }
    764             } else {
    765                 buildingNumber = false;
    766                 builder.append(c);
    767             }
    768         }
    769         return builder.toString();
    770     }
    771 
    772     private void startActivityHelp(Intent intent) {
    773         startActivity(intent);
    774     }
    775 
    776     private void listenForCommand() {
    777         if (Config.LOGD) Log.d(TAG, ""
    778                 + "Command(): MICROPHONE_EXTRA: "+getArg(MICROPHONE_EXTRA)+
    779                 ", CONTACTS_EXTRA: "+getArg(CONTACTS_EXTRA));
    780 
    781         mState = WAITING_FOR_COMMAND;
    782         mRecognizerThread = new Thread() {
    783             public void run() {
    784                 mCommandEngine.recognize(mCommandClient,
    785                         BluetoothVoiceDialerActivity.this,
    786                         newFile(getArg(MICROPHONE_EXTRA)),
    787                         SAMPLE_RATE);
    788             }
    789         };
    790         mRecognizerThread.start();
    791     }
    792 
    793     private void listenForChoice() {
    794         if (Config.LOGD) Log.d(TAG, "listenForChoice(): MICROPHONE_EXTRA: " +
    795                 getArg(MICROPHONE_EXTRA));
    796 
    797         mState = WAITING_FOR_CHOICE;
    798         mRecognizerThread = new Thread() {
    799             public void run() {
    800                 mPhoneTypeChoiceEngine.recognize(mChoiceClient,
    801                         BluetoothVoiceDialerActivity.this,
    802                         newFile(getArg(MICROPHONE_EXTRA)), SAMPLE_RATE);
    803             }
    804         };
    805         mRecognizerThread.start();
    806     }
    807 
    808     private void exitActivity() {
    809         synchronized(this) {
    810             if (mState != EXITING) {
    811                 if (Config.LOGD) Log.d(TAG, "exitActivity");
    812                 mState = SPEAKING_GOODBYE;
    813                 mTtsParams.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,
    814                         GOODBYE_UTTERANCE);
    815                 mTts.speak(getString(R.string.goodbye_tts),
    816                     TextToSpeech.QUEUE_FLUSH,
    817                     mTtsParams);
    818 
    819                 // Normally, the activity will finish() after the
    820                 // utterance completes.  As a fallback in case the utterance
    821                 // does not complete, post a delayed runnable finish the
    822                 // activity.
    823                 mFallbackRunnable = new FallbackRunnable();
    824                 mHandler.postDelayed(mFallbackRunnable, MAX_TTS_DELAY);
    825             }
    826         }
    827     }
    828 
    829     private String getArg(String name) {
    830         if (name == null) return null;
    831         String arg = getIntent().getStringExtra(name);
    832         if (arg != null) return arg;
    833         arg = SystemProperties.get("app.voicedialer." + name);
    834         return arg != null && arg.length() > 0 ? arg : null;
    835     }
    836 
    837     private static File newFile(String name) {
    838         return name != null ? new File(name) : null;
    839     }
    840 
    841     private int playSound(int toneType) {
    842         int msecDelay = 1;
    843 
    844         // use the MediaPlayer to prompt the user
    845         if (mToneGenerator != null) {
    846             mToneGenerator.startTone(toneType);
    847             msecDelay = StrictMath.max(msecDelay, 300);
    848         }
    849         // use the Vibrator to prompt the user
    850         if ((mAudioManager != null) && (mAudioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER))) {
    851             final int VIBRATOR_TIME = 150;
    852             final int VIBRATOR_GUARD_TIME = 150;
    853             Vibrator vibrator = new Vibrator();
    854             vibrator.vibrate(VIBRATOR_TIME);
    855             msecDelay = StrictMath.max(msecDelay,
    856                     VIBRATOR_TIME + VIBRATOR_GUARD_TIME);
    857         }
    858 
    859 
    860         return msecDelay;
    861     }
    862 
    863     protected void onStop() {
    864         if (Config.LOGD) Log.d(TAG, "onStop");
    865 
    866         synchronized(this) {
    867             mState = EXITING;
    868         }
    869 
    870         if (mAlertDialog != null) {
    871             mAlertDialog.dismiss();
    872         }
    873 
    874         // set the volume back to the level it was before we started.
    875         mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO,
    876                                       mBluetoothVoiceVolume, 0);
    877         mAudioManager.abandonAudioFocus(null);
    878 
    879         // shut down bluetooth, if it exists
    880         if (mBluetoothHeadset != null) {
    881             mBluetoothHeadset.stopVoiceRecognition();
    882             mBluetoothHeadset.close();
    883             mBluetoothHeadset = null;
    884         }
    885 
    886         // shut down recognizer and wait for the thread to complete
    887         if (mRecognizerThread !=  null) {
    888             mRecognizerThread.interrupt();
    889             try {
    890                 mRecognizerThread.join();
    891             } catch (InterruptedException e) {
    892                 if (Config.LOGD) Log.d(TAG, "onStop mRecognizerThread.join exception " + e);
    893             }
    894             mRecognizerThread = null;
    895         }
    896 
    897         // clean up UI
    898         mHandler.removeCallbacks(mMicFlasher);
    899         mHandler.removeMessages(0);
    900 
    901         if (mTts != null) {
    902             mTts.stop();
    903             mTts.shutdown();
    904             mTts = null;
    905         }
    906         unregisterReceiver(mReceiver);
    907 
    908         super.onStop();
    909 
    910         // It makes no sense to have this activity maintain state when in
    911         // background.  When it stops, it should just be destroyed.
    912         finish();
    913     }
    914 
    915     private Runnable mMicFlasher = new Runnable() {
    916         int visible = View.VISIBLE;
    917 
    918         public void run() {
    919             findViewById(R.id.microphone_view).setVisibility(visible);
    920             findViewById(R.id.state).setVisibility(visible);
    921             visible = visible == View.VISIBLE ? View.INVISIBLE : View.VISIBLE;
    922             mHandler.postDelayed(this, 750);
    923         }
    924     };
    925 
    926     @Override
    927     protected void onDestroy() {
    928         if (Config.LOGD) Log.d(TAG, "onDestroy");
    929         super.onDestroy();
    930     }
    931 }