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