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 }