• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.voicedialer;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.content.Intent;
23 import android.content.DialogInterface;
24 import android.media.ToneGenerator;
25 import android.media.AudioManager;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.SystemProperties;
29 import android.os.Vibrator;
30 import android.util.Config;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.TextView;
34 import android.widget.Toast;
35 import java.io.File;
36 import java.io.InputStream;
37 import java.io.IOException;
38 
39 /**
40  * TODO: get rid of the anonymous classes
41  * TODO: merge with BluetoothVoiceDialerActivity
42  *
43  * This class is the user interface of the VoiceDialer application.
44  * Its life cycle is as follows:
45  * <ul>
46  * <li>The user presses the recognize key, and the VoiceDialerActivity starts.
47  * <li>A {@link RecognizerEngine} instance is created.
48  * <li>The RecognizerEngine signals the user to speak with the Vibrator.
49  * <li>The RecognizerEngine captures, processes, and recognizes speech
50  * against the names in the contact list.
51  * <li>The RecognizerEngine calls onRecognizerSuccess with a list of
52  * sentences and corresponding Intents.
53  * <li>If the list is one element long, the corresponding Intent is dispatched.
54  * <li>Else an {@link AlertDialog} containing the list of sentences is
55  * displayed.
56  * <li>The user selects the desired sentence from the list,
57  * and the corresponding Intent is dispatched.
58  * <ul>
59  * Notes:
60  * <ul>
61  * <li>The RecognizerEngine is kept and reused for the next recognition cycle.
62  * </ul>
63  */
64 public class VoiceDialerActivity extends Activity {
65 
66     private static final String TAG = "VoiceDialerActivity";
67 
68     private static final String MICROPHONE_EXTRA = "microphone";
69     private static final String CONTACTS_EXTRA = "contacts";
70     private static final String SAMPLE_RATE_EXTRA = "samplerate";
71     private static final String INTENTS_KEY = "intents";
72 
73     private static final int FAIL_PAUSE_MSEC = 5000;
74     private static final int SAMPLE_RATE = 11025;
75 
76     private static final int DIALOG_ID = 1;
77 
78     private final static CommandRecognizerEngine mCommandEngine =
79             new CommandRecognizerEngine();
80     private CommandRecognizerClient mCommandClient;
81     private VoiceDialerTester mVoiceDialerTester;
82     private Handler mHandler;
83     private Thread mRecognizerThread = null;
84     private AudioManager mAudioManager;
85     private ToneGenerator mToneGenerator;
86     private AlertDialog mAlertDialog;
87 
88     @Override
onCreate(Bundle icicle)89     protected void onCreate(Bundle icicle) {
90         if (Config.LOGD) Log.d(TAG, "onCreate");
91         super.onCreate(icicle);
92         mHandler = new Handler();
93         mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
94         mToneGenerator = new ToneGenerator(AudioManager.STREAM_RING,
95                 ToneGenerator.MAX_VOLUME);
96     }
97 
onStart()98     protected void onStart() {
99         if (Config.LOGD) Log.d(TAG, "onStart "  + getIntent());
100         super.onStart();
101         mAudioManager.requestAudioFocus(
102                 null, AudioManager.STREAM_MUSIC,
103                 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
104 
105         mCommandEngine.setContactsFile(newFile(getArg(CONTACTS_EXTRA)));
106         mCommandClient = new CommandRecognizerClient();
107         mCommandEngine.setMinimizeResults(false);
108         mCommandEngine.setAllowOpenEntries(true);
109 
110         // open main window
111         setTheme(android.R.style.Theme_Dialog);
112         setTitle(R.string.title);
113         setContentView(R.layout.voice_dialing);
114         findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
115         findViewById(R.id.retry_view).setVisibility(View.INVISIBLE);
116         findViewById(R.id.microphone_loading_view).setVisibility(View.VISIBLE);
117         if (RecognizerLogger.isEnabled(this)) {
118             ((TextView)findViewById(R.id.substate)).setText(R.string.logging_enabled);
119         }
120 
121         // start the tester, if present
122         mVoiceDialerTester = null;
123         File micDir = newFile(getArg(MICROPHONE_EXTRA));
124         if (micDir != null && micDir.isDirectory()) {
125             mVoiceDialerTester = new VoiceDialerTester(micDir);
126             startNextTest();
127             return;
128         }
129 
130         startWork();
131     }
132 
startWork()133     private void startWork() {
134         // start the engine
135         mRecognizerThread = new Thread() {
136             public void run() {
137                 if (Config.LOGD) Log.d(TAG, "onCreate.Runnable.run");
138                 String sampleRateStr = getArg(SAMPLE_RATE_EXTRA);
139                 int sampleRate = SAMPLE_RATE;
140                 if (sampleRateStr != null) {
141                     sampleRate = Integer.parseInt(sampleRateStr);
142                 }
143                 mCommandEngine.recognize(mCommandClient, VoiceDialerActivity.this,
144                         newFile(getArg(MICROPHONE_EXTRA)),
145                         sampleRate);
146             }
147         };
148         mRecognizerThread.start();
149     }
150 
getArg(String name)151     private String getArg(String name) {
152         if (name == null) return null;
153         String arg = getIntent().getStringExtra(name);
154         if (arg != null) return arg;
155         arg = SystemProperties.get("app.voicedialer." + name);
156         return arg != null && arg.length() > 0 ? arg : null;
157     }
158 
newFile(String name)159     private static File newFile(String name) {
160         return name != null ? new File(name) : null;
161     }
162 
startNextTest()163     private void startNextTest() {
164         mHandler.postDelayed(new Runnable() {
165             public void run() {
166                 if (mVoiceDialerTester == null) {
167                     return;
168                 }
169                 if (!mVoiceDialerTester.stepToNextTest()) {
170                     mVoiceDialerTester.report();
171                     notifyText("Test completed!");
172                     finish();
173                     return;
174                 }
175                 File microphone = mVoiceDialerTester.getWavFile();
176                 File contacts = newFile(getArg(CONTACTS_EXTRA));
177 
178                 notifyText("Testing\n" + microphone + "\n" + contacts);
179                 mCommandEngine.recognize(mCommandClient, VoiceDialerActivity.this,
180                         microphone, SAMPLE_RATE);
181             }
182         }, 2000);
183     }
184 
playSound(int toneType)185     private int playSound(int toneType) {
186         int msecDelay = 1;
187 
188         // use the MediaPlayer to prompt the user
189         if (mToneGenerator != null) {
190             mToneGenerator.startTone(toneType);
191             msecDelay = StrictMath.max(msecDelay, 300);
192         }
193 
194         // use the Vibrator to prompt the user
195         if ((mAudioManager != null) &&
196                 (mAudioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER))) {
197             final int VIBRATOR_TIME = 150;
198             final int VIBRATOR_GUARD_TIME = 150;
199             Vibrator vibrator = new Vibrator();
200             vibrator.vibrate(VIBRATOR_TIME);
201             msecDelay = StrictMath.max(msecDelay,
202                     VIBRATOR_TIME + VIBRATOR_GUARD_TIME);
203         }
204 
205         return msecDelay;
206     }
207 
208     @Override
onStop()209     protected void onStop() {
210         if (Config.LOGD) Log.d(TAG, "onStop");
211 
212         mAudioManager.abandonAudioFocus(null);
213 
214         // no more tester
215         mVoiceDialerTester = null;
216 
217         // shut down recognizer and wait for the thread to complete
218         if (mRecognizerThread !=  null) {
219             mRecognizerThread.interrupt();
220             try {
221                 mRecognizerThread.join();
222             } catch (InterruptedException e) {
223                 if (Config.LOGD) Log.d(TAG, "onStop mRecognizerThread.join exception " + e);
224             }
225             mRecognizerThread = null;
226         }
227 
228         // clean up UI
229         mHandler.removeCallbacks(mMicFlasher);
230         mHandler.removeMessages(0);
231 
232         // clean up ToneGenerator
233         if (mToneGenerator != null) {
234             mToneGenerator.release();
235             mToneGenerator = null;
236         }
237 
238         super.onStop();
239 
240         // It makes no sense to have this activity maintain state when in
241         // background.  When it stops, it should just be destroyed.
242         finish();
243     }
244 
notifyText(final CharSequence msg)245     private void notifyText(final CharSequence msg) {
246         Toast.makeText(VoiceDialerActivity.this, msg, Toast.LENGTH_SHORT).show();
247     }
248 
249     private Runnable mMicFlasher = new Runnable() {
250         int visible = View.VISIBLE;
251 
252         public void run() {
253             findViewById(R.id.microphone_view).setVisibility(visible);
254             findViewById(R.id.state).setVisibility(visible);
255             visible = visible == View.VISIBLE ? View.INVISIBLE : View.VISIBLE;
256             mHandler.postDelayed(this, 750);
257         }
258     };
259 
260 
onCreateDialog(int id, Bundle args)261     protected Dialog onCreateDialog(int id, Bundle args) {
262         final Intent intents[] = (Intent[])args.getParcelableArray(INTENTS_KEY);
263 
264         DialogInterface.OnClickListener clickListener =
265             new DialogInterface.OnClickListener() {
266 
267             public void onClick(DialogInterface dialog, int which) {
268                 if (Config.LOGD) Log.d(TAG, "clickListener.onClick " + which);
269                 startActivityHelp(intents[which]);
270                 dismissDialog(DIALOG_ID);
271                 mAlertDialog = null;
272                 finish();
273             }
274 
275         };
276 
277         DialogInterface.OnCancelListener cancelListener =
278             new DialogInterface.OnCancelListener() {
279 
280             public void onCancel(DialogInterface dialog) {
281                 if (Config.LOGD) Log.d(TAG, "cancelListener.onCancel");
282                 dismissDialog(DIALOG_ID);
283                 mAlertDialog = null;
284                 finish();
285             }
286 
287         };
288 
289         DialogInterface.OnClickListener positiveListener =
290             new DialogInterface.OnClickListener() {
291 
292             public void onClick(DialogInterface dialog, int which) {
293                 if (Config.LOGD) Log.d(TAG, "positiveListener.onClick " + which);
294                 if (intents.length == 1 && which == -1) which = 0;
295                 startActivityHelp(intents[which]);
296                 dismissDialog(DIALOG_ID);
297                 mAlertDialog = null;
298                 finish();
299             }
300 
301         };
302 
303         DialogInterface.OnClickListener negativeListener =
304             new DialogInterface.OnClickListener() {
305 
306             public void onClick(DialogInterface dialog, int which) {
307                 if (Config.LOGD) Log.d(TAG, "negativeListener.onClick " + which);
308                 dismissDialog(DIALOG_ID);
309                 mAlertDialog = null;
310                 finish();
311             }
312 
313         };
314 
315         String[] sentences = new String[intents.length];
316         for (int i = 0; i < intents.length; i++) {
317             sentences[i] = intents[i].getStringExtra(
318                     RecognizerEngine.SENTENCE_EXTRA);
319         }
320 
321         mAlertDialog = intents.length > 1 ?
322                 new AlertDialog.Builder(VoiceDialerActivity.this)
323                 .setTitle(R.string.title)
324                 .setItems(sentences, clickListener)
325                 .setOnCancelListener(cancelListener)
326                 .setNegativeButton(android.R.string.cancel, negativeListener)
327                 .show()
328                 :
329                 new AlertDialog.Builder(VoiceDialerActivity.this)
330                 .setTitle(R.string.title)
331                 .setItems(sentences, clickListener)
332                 .setOnCancelListener(cancelListener)
333                 .setPositiveButton(android.R.string.ok, positiveListener)
334                 .setNegativeButton(android.R.string.cancel, negativeListener)
335                 .show();
336 
337         return mAlertDialog;
338     }
339 
340     private class CommandRecognizerClient implements RecognizerClient {
341         static final int MIN_VOLUME_TO_SKIP = 2;
342         /**
343          * Called by the {@link RecognizerEngine} when the microphone is started.
344          */
onMicrophoneStart(InputStream mic)345         public void onMicrophoneStart(InputStream mic) {
346             if (Config.LOGD) Log.d(TAG, "onMicrophoneStart");
347             playSound(ToneGenerator.TONE_PROP_BEEP);
348 
349             int ringVolume = mAudioManager.getStreamVolume(
350                     AudioManager.STREAM_RING);
351             Log.d(TAG, "ringVolume " + ringVolume);
352 
353             if (ringVolume >= MIN_VOLUME_TO_SKIP) {
354                 // now we're playing a sound, and corrupting the input sample.
355                 // So we need to pull that junk off of the input stream so that the
356                 // recognizer won't see it.
357                 try {
358                     skipBeep(mic);
359                 } catch (java.io.IOException e) {
360                     Log.e(TAG, "IOException " + e);
361                 }
362             } else {
363                 Log.d(TAG, "no tone");
364             }
365 
366             if (mVoiceDialerTester != null) return;
367 
368             mHandler.post(new Runnable() {
369                 public void run() {
370                     findViewById(R.id.microphone_loading_view).setVisibility(View.INVISIBLE);
371                     ((TextView)findViewById(R.id.state)).setText(R.string.listening);
372                     mHandler.post(mMicFlasher);
373                 }
374             });
375         }
376 
377         /**
378          *  Beep detection
379          */
380         private static final int START_WINDOW_MS = 500;  // Beep detection window duration in ms
381         private static final int SINE_FREQ = 400;        // base sine frequency on beep
382         private static final int NUM_PERIODS_BLOCK = 10; // number of sine periods in one energy averaging block
383         private static final int THRESHOLD = 8;          // absolute pseudo energy threshold
384         private static final int START = 0;              // beep detection start
385         private static final int RISING = 1;             // beep rising edge start
386         private static final int TOP = 2;                // beep constant energy detected
387 
skipBeep(InputStream is)388         void skipBeep(InputStream is) throws IOException {
389             int sampleCount = ((SAMPLE_RATE / SINE_FREQ) * NUM_PERIODS_BLOCK);
390             int blockSize = 2 * sampleCount; // energy averaging block
391 
392             if (is == null || blockSize == 0) {
393                 return;
394             }
395 
396             byte[] buf = new byte[blockSize];
397             int maxBytes = 2 * ((START_WINDOW_MS * SAMPLE_RATE) / 1000);
398             maxBytes = ((maxBytes-1) / blockSize + 1) * blockSize;
399 
400             int count = 0;
401             int state = START;  // detection state
402             long prevE = 0; // previous pseudo energy
403             long peak = 0;
404             int threshold =  THRESHOLD*sampleCount;  // absolute energy threshold
405             Log.d(TAG, "blockSize " + blockSize);
406 
407             while (count < maxBytes) {
408                 int cnt = 0;
409                 while (cnt < blockSize) {
410                     int n = is.read(buf, cnt, blockSize-cnt);
411                     if (n < 0) {
412                         throw new java.io.IOException();
413                     }
414                     cnt += n;
415                 }
416 
417                 // compute pseudo energy
418                 cnt = blockSize;
419                 long sumx = 0;
420                 long sumxx = 0;
421                 while (cnt >= 2) {
422                     short smp = (short)((buf[cnt - 1] << 8) + (buf[cnt - 2] & 0xFF));
423                     sumx += smp;
424                     sumxx += smp*smp;
425                     cnt -= 2;
426                 }
427                 long energy = (sumxx*sampleCount - sumx*sumx)/(sampleCount*sampleCount);
428                 Log.d(TAG, "sumx " + sumx + " sumxx " + sumxx + " ee " + energy);
429 
430                 switch (state) {
431                     case START:
432                         if (energy > threshold && energy > (prevE * 2) && prevE != 0) {
433                             // rising edge if energy doubled and > abs threshold
434                             state = RISING;
435                             if (Config.LOGD) Log.d(TAG, "start RISING: " + count +" time: "+ (((1000*count)/2)/SAMPLE_RATE));
436                         }
437                         break;
438                     case RISING:
439                         if (energy < threshold || energy < (prevE / 2)){
440                             // energy fell back below half of previous, back to start
441                             if (Config.LOGD) Log.d(TAG, "back to START: " + count +" time: "+ (((1000*count)/2)/SAMPLE_RATE));
442                             peak = 0;
443                             state = START;
444                         } else if (energy > (prevE / 2) && energy < (prevE * 2)) {
445                             // Start of constant energy
446                             if (Config.LOGD) Log.d(TAG, "start TOP: " + count +" time: "+ (((1000*count)/2)/SAMPLE_RATE));
447                             if (peak < energy) {
448                                 peak = energy;
449                             }
450                             state = TOP;
451                         }
452                         break;
453                     case TOP:
454                         if (energy < threshold || energy < (peak / 2)) {
455                             // e went to less than half of the peak
456                             if (Config.LOGD) Log.d(TAG, "end TOP: " + count +" time: "+ (((1000*count)/2)/SAMPLE_RATE));
457                             return;
458                         }
459                         break;
460                     }
461                 prevE = energy;
462                 count += blockSize;
463             }
464             if (Config.LOGD) Log.d(TAG, "no beep detected, timed out");
465         }
466 
467         /**
468          * Called by the {@link RecognizerEngine} if the recognizer fails.
469          */
onRecognitionFailure(final String msg)470         public void onRecognitionFailure(final String msg) {
471             if (Config.LOGD) Log.d(TAG, "onRecognitionFailure " + msg);
472 
473             // get work off UAPI thread
474             mHandler.post(new Runnable() {
475                 public void run() {
476                     // failure, so beep about it
477                     playSound(ToneGenerator.TONE_PROP_NACK);
478 
479                     mHandler.removeCallbacks(mMicFlasher);
480                     ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again);
481                     findViewById(R.id.state).setVisibility(View.VISIBLE);
482                     findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
483                     findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
484 
485                     if (mVoiceDialerTester != null) {
486                         mVoiceDialerTester.onRecognitionFailure(msg);
487                         startNextTest();
488                         return;
489                     }
490 
491                     mHandler.postDelayed(new Runnable() {
492                         public void run() {
493                             finish();
494                         }
495                     }, FAIL_PAUSE_MSEC);
496                 }
497             });
498         }
499 
500         /**
501          * Called by the {@link RecognizerEngine} on an internal error.
502          */
onRecognitionError(final String msg)503         public void onRecognitionError(final String msg) {
504             if (Config.LOGD) Log.d(TAG, "onRecognitionError " + msg);
505 
506             // get work off UAPI thread
507             mHandler.post(new Runnable() {
508                 public void run() {
509                     // error, so beep about it
510                     playSound(ToneGenerator.TONE_PROP_NACK);
511 
512                     mHandler.removeCallbacks(mMicFlasher);
513                     ((TextView)findViewById(R.id.state)).setText(R.string.please_try_again);
514                     ((TextView)findViewById(R.id.substate)).setText(R.string.recognition_error);
515                     findViewById(R.id.state).setVisibility(View.VISIBLE);
516                     findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
517                     findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
518 
519                     if (mVoiceDialerTester != null) {
520                         mVoiceDialerTester.onRecognitionError(msg);
521                         startNextTest();
522                         return;
523                     }
524 
525                     mHandler.postDelayed(new Runnable() {
526                         public void run() {
527                             finish();
528                         }
529                     }, FAIL_PAUSE_MSEC);
530                 }
531             });
532         }
533 
534         /**
535          * Called by the {@link RecognizerEngine} when is succeeds.  If there is
536          * only one item, then the Intent is dispatched immediately.
537          * If there are more, then an AlertDialog is displayed and the user is
538          * prompted to select.
539          * @param intents a list of Intents corresponding to the sentences.
540          */
onRecognitionSuccess(final Intent[] intents)541         public void onRecognitionSuccess(final Intent[] intents) {
542             if (Config.LOGD) Log.d(TAG, "onRecognitionSuccess " + intents.length);
543 
544             // Pull any intents that are not valid to display in a dialog or
545             // call "startActivity" with.
546             // ACTION_RECOGNIZER_RESULT intents are only used when in Bluetooth
547             // mode, to control the behavior of the voicedialer app, rather
548             // than to actually place calls or open apps.
549             int runnableCount = 0;
550             for (int i=0; i < intents.length; i++) {
551                 if (!RecognizerEngine.ACTION_RECOGNIZER_RESULT.equals(
552                         intents[i].getAction())) {
553                     runnableCount++;
554                 }
555             }
556             Intent runnableIntents[] = new Intent[runnableCount];
557             int j = 0;
558             for (int i=0; i < intents.length; i++) {
559                 if (!RecognizerEngine.ACTION_RECOGNIZER_RESULT.equals(
560                         intents[i].getAction())) {
561                     runnableIntents[j] = intents[i];
562                     j++;
563                 }
564             }
565 
566             if (runnableIntents.length == 0) {
567                 // no usable intents
568                 onRecognitionFailure("No displayable intents");
569                 return;
570             }
571             // repackage our intents as a bundle so that we can pass it into
572             // showDialog.  This in required so that we can handle it when
573             // orientation changes and the activity is destroyed and recreated.
574             final Bundle args = new Bundle();
575             args.putParcelableArray(INTENTS_KEY, runnableIntents);
576 
577             mHandler.post(new Runnable() {
578 
579                 public void run() {
580                     // success, so beep about it
581                     playSound(ToneGenerator.TONE_PROP_ACK);
582 
583                     mHandler.removeCallbacks(mMicFlasher);
584 
585                     showDialog(DIALOG_ID, args);
586 
587                     // start the next test
588                     if (mVoiceDialerTester != null) {
589                         mVoiceDialerTester.onRecognitionSuccess(intents);
590                         startNextTest();
591                         mHandler.postDelayed(new Runnable() {
592                             public void run() {
593                                 dismissDialog(DIALOG_ID);
594                                 mAlertDialog = null;
595                             }
596                         }, 2000);
597                     }
598                 }
599             });
600         }
601     }
602 
603     // post a Toast if not real contacts or microphone
startActivityHelp(Intent intent)604     private void startActivityHelp(Intent intent) {
605         if (getArg(MICROPHONE_EXTRA) == null &&
606                 getArg(CONTACTS_EXTRA) == null) {
607             startActivity(intent);
608         } else {
609             notifyText(intent.
610                     getStringExtra(RecognizerEngine.SENTENCE_EXTRA) +
611                     "\n" + intent.toString());
612         }
613 
614     }
615     @Override
onDestroy()616     protected void onDestroy() {
617         if (Config.LOGD) Log.d(TAG, "onDestroy");
618         super.onDestroy();
619     }
620 }
621