VoiceDialerActivity.java revision be1584eb7e59c391a45aa21fb5e65ada1aedae9a
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 ToneGenerator mToneGenerator; 82 private BluetoothHeadset mBluetoothHeadset; 83 84 @Override 85 protected void onCreate(Bundle icicle) { 86 super.onCreate(icicle); 87 88 if (Config.LOGD) Log.d(TAG, "onCreate"); 89 90 mHandler = new Handler(); 91 mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); 92 93 // tell music player to shut up so we can hear 94 Intent i = new Intent("com.android.music.musicservicecommand"); 95 i.putExtra("command", "pause"); 96 sendBroadcast(i); 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 mBluetoothHeadset = new BluetoothHeadset(this, mBluetoothHeadsetServiceListener); 138 } else { 139 startWork(); 140 } 141 } 142 143 private BluetoothHeadset.ServiceListener mBluetoothHeadsetServiceListener = 144 new BluetoothHeadset.ServiceListener() { 145 public void onServiceConnected() { 146 if (mBluetoothHeadset != null && 147 mBluetoothHeadset.getState() == BluetoothHeadset.STATE_CONNECTED) { 148 mBluetoothHeadset.startVoiceRecognition(); 149 } 150 startWork(); 151 } 152 public void onServiceDisconnected() {} 153 }; 154 155 private void startWork() { 156 // prompt the user with a beep 157 final int msec = playSound(ToneGenerator.TONE_PROP_PROMPT); 158 159 // start the engine after the beep 160 mRecognizerThread = new Thread() { 161 public void run() { 162 if (Config.LOGD) Log.d(TAG, "onCreate.Runnable.run"); 163 try { 164 Thread.sleep(msec); 165 } catch (InterruptedException e) { 166 return; 167 } 168 if (mToneGenerator != null) mToneGenerator.stopTone(); 169 mEngine.recognize(VoiceDialerActivity.this, 170 newFile(getArg(MICROPHONE_EXTRA)), 171 newFile(getArg(CONTACTS_EXTRA)), 172 getArg(CODEC_EXTRA)); 173 } 174 }; 175 mRecognizerThread.start(); 176 } 177 178 /** 179 * Returns a Bundle with the result for a test run 180 * @return Bundle or null if the test is in progress 181 */ 182 public Bundle getRecognitionResult() { 183 return null; 184 } 185 186 private String getArg(String name) { 187 if (name == null) return null; 188 String arg = getIntent().getStringExtra(name); 189 if (arg != null) return arg; 190 arg = SystemProperties.get("app.voicedialer." + name); 191 return arg != null && arg.length() > 0 ? arg : null; 192 } 193 194 private static File newFile(String name) { 195 return name != null ? new File(name) : null; 196 } 197 198 private void startNextTest() { 199 mHandler.postDelayed(new Runnable() { 200 public void run() { 201 if (mVoiceDialerTester == null) { 202 return; 203 } 204 if (!mVoiceDialerTester.stepToNextTest()) { 205 mVoiceDialerTester.report(); 206 notifyText("Test completed!"); 207 finish(); 208 return; 209 } 210 File microphone = mVoiceDialerTester.getWavFile(); 211 File contacts = newFile(getArg(CONTACTS_EXTRA)); 212 String codec = getArg(CODEC_EXTRA); 213 notifyText("Testing\n" + microphone + "\n" + contacts); 214 mEngine.recognize(VoiceDialerActivity.this, 215 microphone, contacts, codec); 216 } 217 }, 2000); 218 } 219 220 private int playSound(int toneType) { 221 int msecDelay = 1; 222 223 // use the MediaPlayer to prompt the user 224 if (mToneGenerator != null) { 225 mToneGenerator.startTone(toneType); 226 msecDelay = StrictMath.max(msecDelay, 300); 227 } 228 229 // use the Vibrator to prompt the user 230 if ((mAudioManager != null) && (mAudioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER))) { 231 final int VIBRATOR_TIME = 150; 232 final int VIBRATOR_GUARD_TIME = 150; 233 Vibrator vibrator = new Vibrator(); 234 vibrator.vibrate(VIBRATOR_TIME); 235 msecDelay = StrictMath.max(msecDelay, 236 VIBRATOR_TIME + VIBRATOR_GUARD_TIME); 237 } 238 239 return msecDelay; 240 } 241 242 @Override 243 protected void onPause() { 244 super.onPause(); 245 246 if (Config.LOGD) Log.d(TAG, "onPause"); 247 248 // shut down bluetooth, if it exists 249 if (mBluetoothHeadset != null) { 250 mBluetoothHeadset.stopVoiceRecognition(); 251 mBluetoothHeadset.close(); 252 mBluetoothHeadset = null; 253 } 254 255 // no more tester 256 mVoiceDialerTester = null; 257 258 // shut down recognizer and wait for the thread to complete 259 if (mRecognizerThread != null) { 260 mRecognizerThread.interrupt(); 261 try { 262 mRecognizerThread.join(); 263 } catch (InterruptedException e) { 264 if (Config.LOGD) Log.d(TAG, "onPause mRecognizerThread.join exception " + e); 265 } 266 mRecognizerThread = null; 267 } 268 269 // clean up UI 270 mHandler.removeCallbacks(mMicFlasher); 271 mHandler.removeMessages(0); 272 273 // clean up ToneGenerator 274 if (mToneGenerator != null) { 275 mToneGenerator.release(); 276 mToneGenerator = null; 277 } 278 279 // bye 280 finish(); 281 } 282 283 private void notifyText(final CharSequence msg) { 284 Toast.makeText(VoiceDialerActivity.this, msg, Toast.LENGTH_SHORT).show(); 285 } 286 287 private Runnable mMicFlasher = new Runnable() { 288 int visible = View.VISIBLE; 289 290 public void run() { 291 findViewById(R.id.microphone_view).setVisibility(visible); 292 findViewById(R.id.state).setVisibility(visible); 293 visible = visible == View.VISIBLE ? View.INVISIBLE : View.VISIBLE; 294 mHandler.postDelayed(this, 750); 295 } 296 }; 297 298 /** 299 * Called by the {@link RecognizerEngine} when the microphone is started. 300 */ 301 public void onMicrophoneStart() { 302 if (Config.LOGD) Log.d(TAG, "onMicrophoneStart"); 303 304 if (mVoiceDialerTester != null) return; 305 306 mHandler.post(new Runnable() { 307 public void run() { 308 findViewById(R.id.microphone_loading_view).setVisibility(View.INVISIBLE); 309 ((TextView)findViewById(R.id.state)).setText(R.string.listening); 310 mHandler.post(mMicFlasher); 311 } 312 }); 313 } 314 315 /** 316 * Called by the {@link RecognizerEngine} if the recognizer fails. 317 */ 318 public void onRecognitionFailure(final String msg) { 319 if (Config.LOGD) Log.d(TAG, "onRecognitionFailure " + msg); 320 321 // get work off UAPI thread 322 mHandler.post(new Runnable() { 323 public void run() { 324 // failure, so beep about it 325 playSound(ToneGenerator.TONE_PROP_NACK); 326 327 mHandler.removeCallbacks(mMicFlasher); 328 ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again); 329 findViewById(R.id.state).setVisibility(View.VISIBLE); 330 findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE); 331 findViewById(R.id.retry_view).setVisibility(View.VISIBLE); 332 333 if (mVoiceDialerTester != null) { 334 mVoiceDialerTester.onRecognitionFailure(msg); 335 startNextTest(); 336 return; 337 } 338 339 mHandler.postDelayed(new Runnable() { 340 public void run() { 341 finish(); 342 } 343 }, FAIL_PAUSE_MSEC); 344 } 345 }); 346 } 347 348 /** 349 * Called by the {@link RecognizerEngine} on an internal error. 350 */ 351 public void onRecognitionError(final String msg) { 352 if (Config.LOGD) Log.d(TAG, "onRecognitionError " + msg); 353 354 // get work off UAPI thread 355 mHandler.post(new Runnable() { 356 public void run() { 357 // error, so beep about it 358 playSound(ToneGenerator.TONE_PROP_NACK); 359 360 mHandler.removeCallbacks(mMicFlasher); 361 ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again); 362 ((TextView)findViewById(R.id.substate)).setText(R.string.recognition_error); 363 findViewById(R.id.state).setVisibility(View.VISIBLE); 364 findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE); 365 findViewById(R.id.retry_view).setVisibility(View.VISIBLE); 366 367 if (mVoiceDialerTester != null) { 368 mVoiceDialerTester.onRecognitionError(msg); 369 startNextTest(); 370 return; 371 } 372 373 mHandler.postDelayed(new Runnable() { 374 public void run() { 375 finish(); 376 } 377 }, FAIL_PAUSE_MSEC); 378 } 379 }); 380 } 381 382 /** 383 * Called by the {@link RecognizerEngine} when is succeeds. If there is 384 * only one item, then the Intent is dispatched immediately. 385 * If there are more, then an AlertDialog is displayed and the user is 386 * prompted to select. 387 * @param intents a list of Intents corresponding to the sentences. 388 */ 389 public void onRecognitionSuccess(final Intent[] intents) { 390 if (Config.LOGD) Log.d(TAG, "onRecognitionSuccess " + intents.length); 391 392 mHandler.post(new Runnable() { 393 394 public void run() { 395 // success, so beep about it 396 playSound(ToneGenerator.TONE_PROP_ACK); 397 398 mHandler.removeCallbacks(mMicFlasher); 399 400 // only one item, so just launch 401 /* 402 if (intents.length == 1 && mVoiceDialerTester == null) { 403 // start the Intent 404 startActivityHelp(intents[0]); 405 finish(); 406 return; 407 } 408 */ 409 410 DialogInterface.OnClickListener clickListener = 411 new DialogInterface.OnClickListener() { 412 413 public void onClick(DialogInterface dialog, int which) { 414 if (Config.LOGD) Log.d(TAG, "clickListener.onClick " + which); 415 startActivityHelp(intents[which]); 416 dialog.dismiss(); 417 finish(); 418 } 419 420 }; 421 422 DialogInterface.OnCancelListener cancelListener = 423 new DialogInterface.OnCancelListener() { 424 425 public void onCancel(DialogInterface dialog) { 426 if (Config.LOGD) Log.d(TAG, "cancelListener.onCancel"); 427 dialog.dismiss(); 428 finish(); 429 } 430 431 }; 432 433 DialogInterface.OnClickListener positiveListener = 434 new DialogInterface.OnClickListener() { 435 436 public void onClick(DialogInterface dialog, int which) { 437 if (Config.LOGD) Log.d(TAG, "positiveListener.onClick " + which); 438 if (intents.length == 1 && which == -1) which = 0; 439 startActivityHelp(intents[which]); 440 dialog.dismiss(); 441 finish(); 442 } 443 444 }; 445 446 DialogInterface.OnClickListener negativeListener = 447 new DialogInterface.OnClickListener() { 448 449 public void onClick(DialogInterface dialog, int which) { 450 if (Config.LOGD) Log.d(TAG, "negativeListener.onClick " + which); 451 dialog.dismiss(); 452 finish(); 453 } 454 455 }; 456 457 String[] sentences = new String[intents.length]; 458 for (int i = 0; i < intents.length; i++) { 459 sentences[i] = intents[i].getStringExtra( 460 RecognizerEngine.SENTENCE_EXTRA); 461 } 462 463 final AlertDialog alertDialog = intents.length > 1 ? 464 new AlertDialog.Builder(VoiceDialerActivity.this) 465 .setTitle(R.string.title) 466 .setItems(sentences, clickListener) 467 .setOnCancelListener(cancelListener) 468 .setNegativeButton(android.R.string.cancel, negativeListener) 469 .show() 470 : 471 new AlertDialog.Builder(VoiceDialerActivity.this) 472 .setTitle(R.string.title) 473 .setItems(sentences, clickListener) 474 .setOnCancelListener(cancelListener) 475 .setPositiveButton(android.R.string.ok, positiveListener) 476 .setNegativeButton(android.R.string.cancel, negativeListener) 477 .show(); 478 479 // start the next test 480 if (mVoiceDialerTester != null) { 481 mVoiceDialerTester.onRecognitionSuccess(intents); 482 startNextTest(); 483 mHandler.postDelayed(new Runnable() { 484 public void run() { 485 alertDialog.dismiss(); 486 } 487 }, 2000); 488 } 489 } 490 491 // post a Toast if not real contacts or microphone 492 private void startActivityHelp(Intent intent) { 493 if (getArg(MICROPHONE_EXTRA) == null && 494 getArg(CONTACTS_EXTRA) == null) { 495 startActivity(intent); 496 } else { 497 notifyText(intent. 498 getStringExtra(RecognizerEngine.SENTENCE_EXTRA) + 499 "\n" + intent.toString()); 500 } 501 502 } 503 504 }); 505 506 } 507 508 @Override 509 protected void onDestroy() { 510 super.onDestroy(); 511 } 512 513 private static class VoiceDialerTester { 514 public VoiceDialerTester(File f) { 515 } 516 517 public boolean stepToNextTest() { 518 return false; 519 } 520 521 public void report() { 522 } 523 524 public File getWavFile() { 525 return null; 526 } 527 528 public void onRecognitionFailure(String msg) { 529 } 530 531 public void onRecognitionError(String err) { 532 } 533 534 public void onRecognitionSuccess(Intent[] intents) { 535 } 536 } 537 538} 539