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