blob: 380d7b7c4e57b6b405eea9d6562a2d9a8415108d [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.voicedialer;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothHeadset;
import android.content.Intent;
import android.content.DialogInterface;
import android.media.ToneGenerator;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemProperties;
import android.os.Vibrator;
import android.telephony.PhoneNumberUtils;
import android.util.Config;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.android.voicedialer.RecognizerEngine;
//import com.android.voicedialer.VoiceDialerTester;
import java.io.File;
/**
* This class is the user interface of the VoiceDialer application.
* Its life cycle is as follows:
* <ul>
* <li>The user presses the recognize key, and the VoiceDialerActivity starts.
* <li>A {@link RecognizerEngine} instance is created.
* <li>The RecognizerEngine signals the user to speak with the Vibrator.
* <li>The RecognizerEngine captures, processes, and recognizes speech
* against the names in the contact list.
* <li>The RecognizerEngine calls onRecognizerSuccess with a list of
* sentences and corresponding Intents.
* <li>If the list is one element long, the corresponding Intent is dispatched.
* <li>Else an {@link AlertDialog} containing the list of sentences is
* displayed.
* <li>The user selects the desired sentence from the list,
* and the corresponding Intent is dispatched.
* <ul>
* Notes:
* <ul>
* <li>The RecognizerEngine is kept and reused for the next recognition cycle.
* </ul>
*/
public class VoiceDialerActivity extends Activity {
private static final String TAG = "VoiceDialerActivity";
private static final String MICROPHONE_EXTRA = "microphone";
private static final String CONTACTS_EXTRA = "contacts";
private static final String CODEC_EXTRA = "codec";
private static final String TONE_EXTRA = "tone";
private static final int FAIL_PAUSE_MSEC = 5000;
private final static RecognizerEngine mEngine = new RecognizerEngine();
private VoiceDialerTester mVoiceDialerTester;
private Handler mHandler;
private Thread mRecognizerThread = null;
private AudioManager mAudioManager;
private ToneGenerator mToneGenerator;
private BluetoothHeadset mBluetoothHeadset;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (Config.LOGD) Log.d(TAG, "onCreate");
mHandler = new Handler();
mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE);
// tell music player to shut up so we can hear
Intent i = new Intent("com.android.music.musicservicecommand");
i.putExtra("command", "pause");
sendBroadcast(i);
// set up ToneGenerator
// currently disabled because it crashes audio input
mToneGenerator = !"0".equals(getArg(TONE_EXTRA)) ?
new ToneGenerator(AudioManager.STREAM_RING, ToneGenerator.MAX_VOLUME) :
null;
// open main window
setTheme(android.R.style.Theme_Dialog);
setTitle(R.string.title);
setContentView(R.layout.voice_dialing);
findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
findViewById(R.id.retry_view).setVisibility(View.INVISIBLE);
findViewById(R.id.microphone_loading_view).setVisibility(View.VISIBLE);
if (RecognizerLogger.isEnabled(this)) {
((TextView)findViewById(R.id.substate)).setText(R.string.logging_enabled);
}
// throw up tooltip
if (false && !Intent.ACTION_VOICE_COMMAND.equals(getIntent().getAction())) {
View v = getLayoutInflater().inflate(R.layout.tool_tip, null);
Toast toast = new Toast(this);
toast.setView(v);
toast.setDuration(Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM, 0, 0);
toast.show();
}
// start the tester, if present
mVoiceDialerTester = null;
File micDir = newFile(getArg(MICROPHONE_EXTRA));
if (micDir != null && micDir.isDirectory()) {
mVoiceDialerTester = new VoiceDialerTester(micDir);
startNextTest();
return;
}
// Get handle to BluetoothHeadset object if required
if (!BluetoothHeadset.DISABLE_BT_VOICE_DIALING &&
Intent.ACTION_VOICE_COMMAND.equals(getIntent().getAction())) {
// start work in the BluetoothHeadsetClient onServiceConnected() callback
mBluetoothHeadset = new BluetoothHeadset(this, mBluetoothHeadsetServiceListener);
} else {
startWork();
}
}
private BluetoothHeadset.ServiceListener mBluetoothHeadsetServiceListener =
new BluetoothHeadset.ServiceListener() {
public void onServiceConnected() {
if (mBluetoothHeadset != null &&
mBluetoothHeadset.getState() == BluetoothHeadset.STATE_CONNECTED) {
mBluetoothHeadset.startVoiceRecognition();
}
startWork();
}
public void onServiceDisconnected() {}
};
private void startWork() {
// start the engine
mRecognizerThread = new Thread() {
public void run() {
if (Config.LOGD) Log.d(TAG, "onCreate.Runnable.run");
mEngine.recognize(VoiceDialerActivity.this,
newFile(getArg(MICROPHONE_EXTRA)),
newFile(getArg(CONTACTS_EXTRA)),
getArg(CODEC_EXTRA));
}
};
mRecognizerThread.start();
}
/**
* Returns a Bundle with the result for a test run
* @return Bundle or null if the test is in progress
*/
public Bundle getRecognitionResult() {
return null;
}
private String getArg(String name) {
if (name == null) return null;
String arg = getIntent().getStringExtra(name);
if (arg != null) return arg;
arg = SystemProperties.get("app.voicedialer." + name);
return arg != null && arg.length() > 0 ? arg : null;
}
private static File newFile(String name) {
return name != null ? new File(name) : null;
}
private void startNextTest() {
mHandler.postDelayed(new Runnable() {
public void run() {
if (mVoiceDialerTester == null) {
return;
}
if (!mVoiceDialerTester.stepToNextTest()) {
mVoiceDialerTester.report();
notifyText("Test completed!");
finish();
return;
}
File microphone = mVoiceDialerTester.getWavFile();
File contacts = newFile(getArg(CONTACTS_EXTRA));
String codec = getArg(CODEC_EXTRA);
notifyText("Testing\n" + microphone + "\n" + contacts);
mEngine.recognize(VoiceDialerActivity.this,
microphone, contacts, codec);
}
}, 2000);
}
private int playSound(int toneType) {
int msecDelay = 1;
// use the MediaPlayer to prompt the user
if (mToneGenerator != null) {
mToneGenerator.startTone(toneType);
msecDelay = StrictMath.max(msecDelay, 300);
}
// use the Vibrator to prompt the user
if ((mAudioManager != null) && (mAudioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER))) {
final int VIBRATOR_TIME = 150;
final int VIBRATOR_GUARD_TIME = 150;
Vibrator vibrator = new Vibrator();
vibrator.vibrate(VIBRATOR_TIME);
msecDelay = StrictMath.max(msecDelay,
VIBRATOR_TIME + VIBRATOR_GUARD_TIME);
}
return msecDelay;
}
@Override
protected void onPause() {
super.onPause();
if (Config.LOGD) Log.d(TAG, "onPause");
// shut down bluetooth, if it exists
if (mBluetoothHeadset != null) {
mBluetoothHeadset.stopVoiceRecognition();
mBluetoothHeadset.close();
mBluetoothHeadset = null;
}
// no more tester
mVoiceDialerTester = null;
// shut down recognizer and wait for the thread to complete
if (mRecognizerThread != null) {
mRecognizerThread.interrupt();
try {
mRecognizerThread.join();
} catch (InterruptedException e) {
if (Config.LOGD) Log.d(TAG, "onPause mRecognizerThread.join exception " + e);
}
mRecognizerThread = null;
}
// clean up UI
mHandler.removeCallbacks(mMicFlasher);
mHandler.removeMessages(0);
// clean up ToneGenerator
if (mToneGenerator != null) {
mToneGenerator.release();
mToneGenerator = null;
}
// bye
finish();
}
private void notifyText(final CharSequence msg) {
Toast.makeText(VoiceDialerActivity.this, msg, Toast.LENGTH_SHORT).show();
}
private Runnable mMicFlasher = new Runnable() {
int visible = View.VISIBLE;
public void run() {
findViewById(R.id.microphone_view).setVisibility(visible);
findViewById(R.id.state).setVisibility(visible);
visible = visible == View.VISIBLE ? View.INVISIBLE : View.VISIBLE;
mHandler.postDelayed(this, 750);
}
};
/**
* Called by the {@link RecognizerEngine} when the microphone is started.
*/
public void onMicrophoneStart() {
if (Config.LOGD) Log.d(TAG, "onMicrophoneStart");
if (mVoiceDialerTester != null) return;
mHandler.post(new Runnable() {
public void run() {
findViewById(R.id.microphone_loading_view).setVisibility(View.INVISIBLE);
((TextView)findViewById(R.id.state)).setText(R.string.listening);
mHandler.post(mMicFlasher);
}
});
}
/**
* Called by the {@link RecognizerEngine} if the recognizer fails.
*/
public void onRecognitionFailure(final String msg) {
if (Config.LOGD) Log.d(TAG, "onRecognitionFailure " + msg);
// get work off UAPI thread
mHandler.post(new Runnable() {
public void run() {
// failure, so beep about it
playSound(ToneGenerator.TONE_PROP_NACK);
mHandler.removeCallbacks(mMicFlasher);
((TextView)findViewById(R.id.state)).setText(R.string.please_try_again);
findViewById(R.id.state).setVisibility(View.VISIBLE);
findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
if (mVoiceDialerTester != null) {
mVoiceDialerTester.onRecognitionFailure(msg);
startNextTest();
return;
}
mHandler.postDelayed(new Runnable() {
public void run() {
finish();
}
}, FAIL_PAUSE_MSEC);
}
});
}
/**
* Called by the {@link RecognizerEngine} on an internal error.
*/
public void onRecognitionError(final String msg) {
if (Config.LOGD) Log.d(TAG, "onRecognitionError " + msg);
// get work off UAPI thread
mHandler.post(new Runnable() {
public void run() {
// error, so beep about it
playSound(ToneGenerator.TONE_PROP_NACK);
mHandler.removeCallbacks(mMicFlasher);
((TextView)findViewById(R.id.state)).setText(R.string.please_try_again);
((TextView)findViewById(R.id.substate)).setText(R.string.recognition_error);
findViewById(R.id.state).setVisibility(View.VISIBLE);
findViewById(R.id.microphone_view).setVisibility(View.INVISIBLE);
findViewById(R.id.retry_view).setVisibility(View.VISIBLE);
if (mVoiceDialerTester != null) {
mVoiceDialerTester.onRecognitionError(msg);
startNextTest();
return;
}
mHandler.postDelayed(new Runnable() {
public void run() {
finish();
}
}, FAIL_PAUSE_MSEC);
}
});
}
/**
* Called by the {@link RecognizerEngine} when is succeeds. If there is
* only one item, then the Intent is dispatched immediately.
* If there are more, then an AlertDialog is displayed and the user is
* prompted to select.
* @param intents a list of Intents corresponding to the sentences.
*/
public void onRecognitionSuccess(final Intent[] intents) {
if (Config.LOGD) Log.d(TAG, "onRecognitionSuccess " + intents.length);
mHandler.post(new Runnable() {
public void run() {
// success, so beep about it
playSound(ToneGenerator.TONE_PROP_ACK);
mHandler.removeCallbacks(mMicFlasher);
// only one item, so just launch
/*
if (intents.length == 1 && mVoiceDialerTester == null) {
// start the Intent
startActivityHelp(intents[0]);
finish();
return;
}
*/
DialogInterface.OnClickListener clickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (Config.LOGD) Log.d(TAG, "clickListener.onClick " + which);
startActivityHelp(intents[which]);
dialog.dismiss();
finish();
}
};
DialogInterface.OnCancelListener cancelListener =
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (Config.LOGD) Log.d(TAG, "cancelListener.onCancel");
dialog.dismiss();
finish();
}
};
DialogInterface.OnClickListener positiveListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (Config.LOGD) Log.d(TAG, "positiveListener.onClick " + which);
if (intents.length == 1 && which == -1) which = 0;
startActivityHelp(intents[which]);
dialog.dismiss();
finish();
}
};
DialogInterface.OnClickListener negativeListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (Config.LOGD) Log.d(TAG, "negativeListener.onClick " + which);
dialog.dismiss();
finish();
}
};
String[] sentences = new String[intents.length];
for (int i = 0; i < intents.length; i++) {
sentences[i] = intents[i].getStringExtra(
RecognizerEngine.SENTENCE_EXTRA);
}
final AlertDialog alertDialog = intents.length > 1 ?
new AlertDialog.Builder(VoiceDialerActivity.this)
.setTitle(R.string.title)
.setItems(sentences, clickListener)
.setOnCancelListener(cancelListener)
.setNegativeButton(android.R.string.cancel, negativeListener)
.show()
:
new AlertDialog.Builder(VoiceDialerActivity.this)
.setTitle(R.string.title)
.setItems(sentences, clickListener)
.setOnCancelListener(cancelListener)
.setPositiveButton(android.R.string.ok, positiveListener)
.setNegativeButton(android.R.string.cancel, negativeListener)
.show();
// start the next test
if (mVoiceDialerTester != null) {
mVoiceDialerTester.onRecognitionSuccess(intents);
startNextTest();
mHandler.postDelayed(new Runnable() {
public void run() {
alertDialog.dismiss();
}
}, 2000);
}
}
// post a Toast if not real contacts or microphone
private void startActivityHelp(Intent intent) {
if (getArg(MICROPHONE_EXTRA) == null &&
getArg(CONTACTS_EXTRA) == null) {
startActivity(intent);
} else {
notifyText(intent.
getStringExtra(RecognizerEngine.SENTENCE_EXTRA) +
"\n" + intent.toString());
}
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private static class VoiceDialerTester {
public VoiceDialerTester(File f) {
}
public boolean stepToNextTest() {
return false;
}
public void report() {
}
public File getWavFile() {
return null;
}
public void onRecognitionFailure(String msg) {
}
public void onRecognitionError(String err) {
}
public void onRecognitionSuccess(Intent[] intents) {
}
}
}