VoiceDialerActivity.java revision 3e6a72b9c088bc6c65d9e162d2ba31dd7100e3a6
1/* 2 * Copyright (C) 2007 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 17package com.android.voicedialer; 18 19 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.bluetooth.BluetoothHeadset; 23import android.content.Intent; 24import android.content.DialogInterface; 25import android.media.ToneGenerator; 26import android.media.AudioManager; 27import android.os.Bundle; 28import android.os.Handler; 29import android.os.SystemProperties; 30import android.os.Vibrator; 31import android.telephony.PhoneNumberUtils; 32import android.util.Config; 33import android.util.Log; 34import android.view.Gravity; 35import android.view.View; 36import android.widget.TextView; 37import android.widget.Toast; 38import com.android.voicedialer.RecognizerEngine; 39//import com.android.voicedialer.VoiceDialerTester; 40import java.io.File; 41 42 43/** 44 * This class is the user interface of the VoiceDialer application. 45 * Its life cycle is as follows: 46 * <ul> 47 * <li>The user presses the recognize key, and the VoiceDialerActivity starts. 48 * <li>A {@link RecognizerEngine} instance is created. 49 * <li>The RecognizerEngine signals the user to speak with the Vibrator. 50 * <li>The RecognizerEngine captures, processes, and recognizes speech 51 * against the names in the contact list. 52 * <li>The RecognizerEngine calls onRecognizerSuccess with a list of 53 * sentences and corresponding Intents. 54 * <li>If the list is one element long, the corresponding Intent is dispatched. 55 * <li>Else an {@link AlertDialog} containing the list of sentences is 56 * displayed. 57 * <li>The user selects the desired sentence from the list, 58 * and the corresponding Intent is dispatched. 59 * <ul> 60 * Notes: 61 * <ul> 62 * <li>The RecognizerEngine is kept and reused for the next recognition cycle. 63 * </ul> 64 */ 65public class VoiceDialerActivity extends Activity { 66 67 private static final String TAG = "VoiceDialerActivity"; 68 69 private static final String MICROPHONE_EXTRA = "microphone"; 70 private static final String CONTACTS_EXTRA = "contacts"; 71 private static final String CODEC_EXTRA = "codec"; 72 private static final String TONE_EXTRA = "tone"; 73 74 private static final int FAIL_PAUSE_MSEC = 5000; 75 76 private final static RecognizerEngine mEngine = new RecognizerEngine(); 77 private VoiceDialerTester mVoiceDialerTester; 78 private Handler mHandler; 79 private Thread mRecognizerThread = null; 80 private AudioManager mAudioManager; 81 private int mSavedVolume; 82 private ToneGenerator mToneGenerator; 83 private BluetoothHeadset mBluetoothHeadset; 84 85 @Override 86 protected void onCreate(Bundle icicle) { 87 super.onCreate(icicle); 88 89 if (Config.LOGD) Log.d(TAG, "onCreate"); 90 91 mHandler = new Handler(); 92 93 // get AudioManager, save current music volume, set music volume to zero 94 mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); 95 mSavedVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 96 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0); 97 98 // set up ToneGenerator 99 // currently disabled because it crashes audio input 100 mToneGenerator = !"0".equals(getArg(TONE_EXTRA)) ? 101 new ToneGenerator(AudioManager.STREAM_RING, ToneGenerator.MAX_VOLUME) : 102 null; 103 104 // open main window 105 setTheme(android.R.style.Theme_Dialog); 106 setTitle(R.string.title); 107 setContentView(R.layout.voice_dialing); 108 findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE); 109 findViewById(R.id.retry_view).setVisibility(View.INVISIBLE); 110 findViewById(R.id.microphone_loading_view).setVisibility(View.VISIBLE); 111 if (RecognizerLogger.isEnabled(this)) { 112 ((TextView)findViewById(R.id.substate)).setText(R.string.logging_enabled); 113 } 114 115 // throw up tooltip 116 if (false && !Intent.ACTION_VOICE_COMMAND.equals(getIntent().getAction())) { 117 View v = getLayoutInflater().inflate(R.layout.tool_tip, null); 118 Toast toast = new Toast(this); 119 toast.setView(v); 120 toast.setDuration(Toast.LENGTH_LONG); 121 toast.setGravity(Gravity.BOTTOM, 0, 0); 122 toast.show(); 123 } 124 125 // start the tester, if present 126 mVoiceDialerTester = null; 127 File micDir = newFile(getArg(MICROPHONE_EXTRA)); 128 if (micDir != null && micDir.isDirectory()) { 129 mVoiceDialerTester = new VoiceDialerTester(micDir); 130 startNextTest(); 131 return; 132 } 133 134 // Get handle to BluetoothHeadset object if required 135 if (Intent.ACTION_VOICE_COMMAND.equals(getIntent().getAction()) && 136 // start work in the BluetoothHeadsetClient onServiceConnected() callback 137 getIntent().getIntExtra(Intent.EXTRA_AUDIO_ROUTE, -1) == 138 AudioManager.ROUTE_BLUETOOTH_SCO) { 139 mBluetoothHeadset = new BluetoothHeadset(this, mBluetoothHeadsetServiceListener); 140 } else { 141 startWork(); 142 } 143 } 144 145 private BluetoothHeadset.ServiceListener mBluetoothHeadsetServiceListener = 146 new BluetoothHeadset.ServiceListener() { 147 public void onServiceConnected() { 148 if (mBluetoothHeadset != null) { 149 mBluetoothHeadset.startVoiceRecognition(); 150 startWork(); 151 } 152 } 153 public void onServiceDisconnected() {} 154 }; 155 156 private void startWork() { 157 // prompt the user with a beep 158 final int msec = playSound(ToneGenerator.TONE_PROP_PROMPT); 159 160 // start the engine after the beep 161 mRecognizerThread = new Thread() { 162 public void run() { 163 if (Config.LOGD) Log.d(TAG, "onCreate.Runnable.run"); 164 try { 165 Thread.sleep(msec); 166 } catch (InterruptedException e) { 167 return; 168 } 169 if (mToneGenerator != null) mToneGenerator.stopTone(); 170 mEngine.recognize(VoiceDialerActivity.this, 171 newFile(getArg(MICROPHONE_EXTRA)), 172 newFile(getArg(CONTACTS_EXTRA)), 173 getArg(CODEC_EXTRA)); 174 } 175 }; 176 mRecognizerThread.start(); 177 } 178 179 /** 180 * Returns a Bundle with the result for a test run 181 * @return Bundle or null if the test is in progress 182 */ 183 public Bundle getRecognitionResult() { 184 return null; 185 } 186 187 private String getArg(String name) { 188 if (name == null) return null; 189 String arg = getIntent().getStringExtra(name); 190 if (arg != null) return arg; 191 arg = SystemProperties.get("app.voicedialer." + name); 192 return arg != null && arg.length() > 0 ? arg : null; 193 } 194 195 private static File newFile(String name) { 196 return name != null ? new File(name) : null; 197 } 198 199 private void startNextTest() { 200 mHandler.postDelayed(new Runnable() { 201 public void run() { 202 if (mVoiceDialerTester == null) { 203 return; 204 } 205 if (!mVoiceDialerTester.stepToNextTest()) { 206 mVoiceDialerTester.report(); 207 notifyText("Test completed!"); 208 finish(); 209 return; 210 } 211 File microphone = mVoiceDialerTester.getWavFile(); 212 File contacts = newFile(getArg(CONTACTS_EXTRA)); 213 String codec = getArg(CODEC_EXTRA); 214 notifyText("Testing\n" + microphone + "\n" + contacts); 215 mEngine.recognize(VoiceDialerActivity.this, 216 microphone, contacts, codec); 217 } 218 }, 2000); 219 } 220 221 private int playSound(int toneType) { 222 int msecDelay = 1; 223 224 // use the MediaPlayer to prompt the user 225 if (mToneGenerator != null) { 226 mToneGenerator.startTone(toneType); 227 msecDelay = StrictMath.max(msecDelay, 300); 228 } 229 230 // use the Vibrator to prompt the user 231 if (mAudioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER)) { 232 final int VIBRATOR_TIME = 150; 233 final int VIBRATOR_GUARD_TIME = 150; 234 Vibrator vibrator = new Vibrator(); 235 vibrator.vibrate(VIBRATOR_TIME); 236 msecDelay = StrictMath.max(msecDelay, 237 VIBRATOR_TIME + VIBRATOR_GUARD_TIME); 238 } 239 240 return msecDelay; 241 } 242 243 @Override 244 protected void onPause() { 245 super.onPause(); 246 247 if (Config.LOGD) Log.d(TAG, "onPause"); 248 249 // shut down bluetooth, if it exists 250 if (mBluetoothHeadset != null) { 251 mBluetoothHeadset.stopVoiceRecognition(); 252 mBluetoothHeadset.close(); 253 mBluetoothHeadset = null; 254 } 255 256 // restore volume, if changed 257 if (mSavedVolume > 0) { 258 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mSavedVolume, 0); 259 mSavedVolume = 0; 260 } 261 262 // no more tester 263 mVoiceDialerTester = null; 264 265 // shut down recognizer and wait for the thread to complete 266 if (mRecognizerThread != null) { 267 mRecognizerThread.interrupt(); 268 try { 269 mRecognizerThread.join(); 270 } catch (InterruptedException e) { 271 if (Config.LOGD) Log.d(TAG, "onPause mRecognizerThread.join exception " + e); 272 } 273 mRecognizerThread = null; 274 } 275 276 // clean up UI 277 mHandler.removeCallbacks(mMicFlasher); 278 mHandler.removeMessages(0); 279 280 // clean up ToneGenerator 281 if (mToneGenerator != null) { 282 mToneGenerator.release(); 283 mToneGenerator = null; 284 } 285 286 // bye 287 finish(); 288 } 289 290 private void notifyText(final CharSequence msg) { 291 Toast.makeText(VoiceDialerActivity.this, msg, Toast.LENGTH_SHORT).show(); 292 } 293 294 private Runnable mMicFlasher = new Runnable() { 295 int visible = View.VISIBLE; 296 297 public void run() { 298 findViewById(R.id.microphone_view).setVisibility(visible); 299 findViewById(R.id.state).setVisibility(visible); 300 visible = visible == View.VISIBLE ? View.INVISIBLE : View.VISIBLE; 301 mHandler.postDelayed(this, 750); 302 } 303 }; 304 305 /** 306 * Called by the {@link RecognizerEngine} when the microphone is started. 307 */ 308 public void onMicrophoneStart() { 309 if (Config.LOGD) Log.d(TAG, "onMicrophoneStart"); 310 311 if (mVoiceDialerTester != null) return; 312 313 mHandler.post(new Runnable() { 314 public void run() { 315 findViewById(R.id.microphone_loading_view).setVisibility(View.INVISIBLE); 316 ((TextView)findViewById(R.id.state)).setText(R.string.listening); 317 mHandler.post(mMicFlasher); 318 } 319 }); 320 } 321 322 /** 323 * Called by the {@link RecognizerEngine} if the recognizer fails. 324 */ 325 public void onRecognitionFailure(final String msg) { 326 if (Config.LOGD) Log.d(TAG, "onRecognitionFailure " + msg); 327 328 // get work off UAPI thread 329 mHandler.post(new Runnable() { 330 public void run() { 331 // failure, so beep about it 332 playSound(ToneGenerator.TONE_PROP_NACK); 333 334 mHandler.removeCallbacks(mMicFlasher); 335 ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again); 336 findViewById(R.id.state).setVisibility(View.VISIBLE); 337 findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE); 338 findViewById(R.id.retry_view).setVisibility(View.VISIBLE); 339 340 if (mVoiceDialerTester != null) { 341 mVoiceDialerTester.onRecognitionFailure(msg); 342 startNextTest(); 343 return; 344 } 345 346 mHandler.postDelayed(new Runnable() { 347 public void run() { 348 finish(); 349 } 350 }, FAIL_PAUSE_MSEC); 351 } 352 }); 353 } 354 355 /** 356 * Called by the {@link RecognizerEngine} on an internal error. 357 */ 358 public void onRecognitionError(final String msg) { 359 if (Config.LOGD) Log.d(TAG, "onRecognitionError " + msg); 360 361 // get work off UAPI thread 362 mHandler.post(new Runnable() { 363 public void run() { 364 // error, so beep about it 365 playSound(ToneGenerator.TONE_PROP_NACK); 366 367 mHandler.removeCallbacks(mMicFlasher); 368 ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again); 369 ((TextView)findViewById(R.id.substate)).setText(R.string.recognition_error); 370 findViewById(R.id.state).setVisibility(View.VISIBLE); 371 findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE); 372 findViewById(R.id.retry_view).setVisibility(View.VISIBLE); 373 374 if (mVoiceDialerTester != null) { 375 mVoiceDialerTester.onRecognitionError(msg); 376 startNextTest(); 377 return; 378 } 379 380 mHandler.postDelayed(new Runnable() { 381 public void run() { 382 finish(); 383 } 384 }, FAIL_PAUSE_MSEC); 385 } 386 }); 387 } 388 389 /** 390 * Called by the {@link RecognizerEngine} when is succeeds. If there is 391 * only one item, then the Intent is dispatched immediately. 392 * If there are more, then an AlertDialog is displayed and the user is 393 * prompted to select. 394 * @param intents a list of Intents corresponding to the sentences. 395 */ 396 public void onRecognitionSuccess(final Intent[] intents) { 397 if (Config.LOGD) Log.d(TAG, "onRecognitionSuccess " + intents.length); 398 399 mHandler.post(new Runnable() { 400 401 public void run() { 402 // success, so beep about it 403 playSound(ToneGenerator.TONE_PROP_ACK); 404 405 mHandler.removeCallbacks(mMicFlasher); 406 407 // only one item, so just launch 408 /* 409 if (intents.length == 1 && mVoiceDialerTester == null) { 410 // start the Intent 411 startActivityHelp(intents[0]); 412 finish(); 413 return; 414 } 415 */ 416 417 DialogInterface.OnClickListener clickListener = 418 new DialogInterface.OnClickListener() { 419 420 public void onClick(DialogInterface dialog, int which) { 421 if (Config.LOGD) Log.d(TAG, "clickListener.onClick " + which); 422 startActivityHelp(intents[which]); 423 dialog.dismiss(); 424 finish(); 425 } 426 427 }; 428 429 DialogInterface.OnCancelListener cancelListener = 430 new DialogInterface.OnCancelListener() { 431 432 public void onCancel(DialogInterface dialog) { 433 if (Config.LOGD) Log.d(TAG, "cancelListener.onCancel"); 434 dialog.dismiss(); 435 finish(); 436 } 437 438 }; 439 440 DialogInterface.OnClickListener positiveListener = 441 new DialogInterface.OnClickListener() { 442 443 public void onClick(DialogInterface dialog, int which) { 444 if (Config.LOGD) Log.d(TAG, "positiveListener.onClick " + which); 445 if (intents.length == 1 && which == -1) which = 0; 446 startActivityHelp(intents[which]); 447 dialog.dismiss(); 448 finish(); 449 } 450 451 }; 452 453 DialogInterface.OnClickListener negativeListener = 454 new DialogInterface.OnClickListener() { 455 456 public void onClick(DialogInterface dialog, int which) { 457 if (Config.LOGD) Log.d(TAG, "negativeListener.onClick " + which); 458 dialog.dismiss(); 459 finish(); 460 } 461 462 }; 463 464 String[] sentences = new String[intents.length]; 465 for (int i = 0; i < intents.length; i++) { 466 sentences[i] = intents[i].getStringExtra( 467 RecognizerEngine.SENTENCE_EXTRA); 468 } 469 470 final AlertDialog alertDialog = intents.length > 1 ? 471 new AlertDialog.Builder(VoiceDialerActivity.this) 472 .setTitle(R.string.title) 473 .setItems(sentences, clickListener) 474 .setOnCancelListener(cancelListener) 475 .setNegativeButton(android.R.string.cancel, negativeListener) 476 .show() 477 : 478 new AlertDialog.Builder(VoiceDialerActivity.this) 479 .setTitle(R.string.title) 480 .setItems(sentences, clickListener) 481 .setOnCancelListener(cancelListener) 482 .setPositiveButton(android.R.string.ok, positiveListener) 483 .setNegativeButton(android.R.string.cancel, negativeListener) 484 .show(); 485 486 // start the next test 487 if (mVoiceDialerTester != null) { 488 mVoiceDialerTester.onRecognitionSuccess(intents); 489 startNextTest(); 490 mHandler.postDelayed(new Runnable() { 491 public void run() { 492 alertDialog.dismiss(); 493 } 494 }, 2000); 495 } 496 } 497 498 // post a Toast if not real contacts or microphone 499 private void startActivityHelp(Intent intent) { 500 if (getArg(MICROPHONE_EXTRA) == null && 501 getArg(CONTACTS_EXTRA) == null) { 502 startActivity(intent); 503 } else { 504 notifyText(intent. 505 getStringExtra(RecognizerEngine.SENTENCE_EXTRA) + 506 "\n" + intent.toString()); 507 } 508 509 } 510 511 }); 512 513 } 514 515 @Override 516 protected void onDestroy() { 517 super.onDestroy(); 518 } 519 520 private static class VoiceDialerTester { 521 public VoiceDialerTester(File f) { 522 } 523 524 public boolean stepToNextTest() { 525 return false; 526 } 527 528 public void report() { 529 } 530 531 public File getWavFile() { 532 return null; 533 } 534 535 public void onRecognitionFailure(String msg) { 536 } 537 538 public void onRecognitionError(String err) { 539 } 540 541 public void onRecognitionSuccess(Intent[] intents) { 542 } 543 } 544 545} 546