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