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