package ee.ioc.phon.android.speak.view; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Paint; import android.os.Bundle; import android.preference.PreferenceManager; import android.speech.RecognitionListener; import android.speech.SpeechRecognizer; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.List; import ee.ioc.phon.android.speak.Log; import ee.ioc.phon.android.speak.OnSwipeTouchListener; import ee.ioc.phon.android.speak.R; import ee.ioc.phon.android.speak.ServiceLanguageChooser; import ee.ioc.phon.android.speak.activity.ComboSelectorActivity; import ee.ioc.phon.android.speak.model.CallerInfo; import ee.ioc.phon.android.speak.model.Combo; import ee.ioc.phon.android.speechutils.Extras; import ee.ioc.phon.android.speechutils.utils.IntentUtils; import ee.ioc.phon.android.speechutils.utils.PreferenceUtils; import ee.ioc.phon.android.speechutils.view.MicButton; public class SpeechInputView extends LinearLayout { private MicButton mBImeStartStop; private ImageButton mBImeKeyboard; private Button mBComboSelector; private TextView mTvInstruction; private TextView mTvMessage; private SpeechInputViewListener mListener; private SpeechRecognizer mRecognizer; private ServiceLanguageChooser mSlc; private MicButton.State mState; public interface SpeechInputViewListener { void onComboChange(String language, ComponentName service); void onPartialResult(List<String> text); void onFinalResult(List<String> text, Bundle bundle); /** * Switch to the next IME or ask the user to choose the IME. * * @param isAskUser */ void onSwitchIme(boolean isAskUser); /** * Switch to the previous IME (the IME that launched this IME) */ void onSwitchToLastIme(); void onSearch(); void onDeleteLastWord(); void goUp(); void goDown(); void onAddNewline(); void onAddSpace(); void onSelectAll(); void onReset(); void onBufferReceived(byte[] buffer); void onError(int errorCode); void onStartListening(); void onStopListening(); } public SpeechInputView(Context context, AttributeSet attrs) { super(context, attrs); } public void setListener(final SpeechInputViewListener listener) { mListener = listener; ImageButton buttonSearch = (ImageButton) findViewById(R.id.bImeSearch); if (mBImeKeyboard != null && buttonSearch != null) { mBImeKeyboard.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mListener.onSwitchToLastIme(); } }); mBImeKeyboard.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { mListener.onSwitchIme(false); return true; } }); buttonSearch.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { cancelOrDestroy(); mListener.onSearch(); } }); buttonSearch.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { // TODO: do something interesting return true; } }); } setOnTouchListener(new OnSwipeTouchListener(getContext()) { @Override public void onSwipeLeft() { mListener.onDeleteLastWord(); } @Override public void onSwipeRight() { mListener.onAddNewline(); } @Override public void onSwipeUp() { mListener.goUp(); } @Override public void onSwipeDown() { mListener.goDown(); } @Override public void onSingleTapMotion() { mListener.onReset(); } @Override public void onDoubleTapMotion() { mListener.onAddSpace(); } @Override public void onLongPressMotion() { mListener.onSelectAll(); } }); mListener.onComboChange(mSlc.getLanguage(), mSlc.getService()); } public void init(int keys, CallerInfo callerInfo) { mBImeStartStop = (MicButton) findViewById(R.id.bImeStartStop); mBImeKeyboard = (ImageButton) findViewById(R.id.bImeKeyboard); mBComboSelector = (Button) findViewById(R.id.tvComboSelector); mTvInstruction = (TextView) findViewById(R.id.tvInstruction); mTvMessage = (TextView) findViewById(R.id.tvMessage); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); // TODO: check for null? (test by deinstalling a recognizer but not changing K6nele settings) mSlc = new ServiceLanguageChooser(getContext(), prefs, keys, callerInfo); if (mSlc.size() > 1) { mBComboSelector.setVisibility(View.VISIBLE); } else { mBComboSelector.setVisibility(View.GONE); } updateServiceLanguage(mSlc.getSpeechRecognizer()); updateComboSelector(mSlc); showMessage(""); setGuiInitState(0); TypedArray keysAsTypedArray = getResources().obtainTypedArray(keys); final int key = keysAsTypedArray.getResourceId(0, 0); int keyHelpText = keysAsTypedArray.getResourceId(8, 0); int defaultHelpText = keysAsTypedArray.getResourceId(9, 0); keysAsTypedArray.recycle(); if (mTvInstruction != null) { if (PreferenceUtils.getPrefBoolean(prefs, getResources(), keyHelpText, defaultHelpText)) { mTvInstruction.setVisibility(View.VISIBLE); } else { mTvInstruction.setVisibility(View.GONE); } } // This button can be pressed in any state. mBImeStartStop.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { Log.i("Microphone button pressed: state = " + mState); switch (mState) { case INIT: case ERROR: startListening(mSlc); break; case RECORDING: stopListening(); break; case LISTENING: case TRANSCRIBING: cancelOrDestroy(); setGuiInitState(0); break; default: } } }); mBComboSelector.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mState == MicButton.State.RECORDING) { stopListening(); } mSlc.next(); mListener.onComboChange(mSlc.getLanguage(), mSlc.getService()); updateComboSelector(mSlc); } }); mBComboSelector.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View view) { cancelOrDestroy(); Context context = getContext(); Intent intent = new Intent(context, ComboSelectorActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("key", context.getString(key)); IntentUtils.startActivityIfAvailable(context, intent); return true; } }); } public void start() { if (mState == MicButton.State.INIT || mState == MicButton.State.ERROR) { // TODO: fix this startListening(mSlc); } } // TODO: make public? private void stopListening() { if (mRecognizer != null) { mRecognizer.stopListening(); } mListener.onStopListening(); } public void cancel() { cancelOrDestroy(); setGuiInitState(0); } public void showMessage(CharSequence message) { if (mTvMessage != null) { if (message == null || message.length() == 0) { setText(mTvMessage, ""); } else { mTvMessage.setEllipsize(TextUtils.TruncateAt.START); mTvMessage.setPaintFlags(mTvMessage.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG & ~Paint.UNDERLINE_TEXT_FLAG); setText(mTvMessage, message); } } } public void showMessage(CharSequence message, boolean isSuccess) { if (mTvMessage != null) { if (message == null || message.length() == 0) { setText(mTvMessage, ""); } else { mTvMessage.setEllipsize(TextUtils.TruncateAt.MIDDLE); if (isSuccess) { mTvMessage.setPaintFlags(mTvMessage.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG) | Paint.UNDERLINE_TEXT_FLAG); } else { mTvMessage.setPaintFlags(mTvMessage.getPaintFlags() & (~Paint.UNDERLINE_TEXT_FLAG) | Paint.STRIKE_THRU_TEXT_FLAG); } setText(mTvMessage, message); } } } private static String selectFirstResult(List<String> results) { if (results == null || results.size() < 1) { return null; } return results.get(0); } private void setGuiState(MicButton.State state) { mState = state; setMicButtonState(mBImeStartStop, mState); } private void setGuiInitState(int message) { if (message == 0) { // Do not clear a possible error message //showMessage(""); setGuiState(MicButton.State.INIT); } else { setGuiState(MicButton.State.ERROR); showMessage(String.format(getResources().getString(R.string.labelSpeechInputViewMessage), getResources().getString(message))); } if (mBImeKeyboard != null) { setVisibility(mBImeKeyboard, View.VISIBLE); } setText(mTvInstruction, R.string.buttonImeSpeak); } private static String lastChars(List<String> results, boolean isFinal) { return lastChars(selectFirstResult(results), isFinal); } private static String lastChars(String str, boolean isFinal) { if (str == null) { str = ""; } else { str = str.replaceAll("\\n", "↲"); } if (isFinal) { return str + "▪"; } return str; } private static void setText(final TextView textView, final CharSequence text) { if (textView != null) { textView.post(new Runnable() { @Override public void run() { textView.setText(text); } }); } } private static void setText(final TextView textView, final int text) { if (textView != null) { textView.post(new Runnable() { @Override public void run() { textView.setText(text); } }); } } private static void setMicButtonVolumeLevel(final MicButton button, final float rmsdB) { if (button != null) { button.post(new Runnable() { @Override public void run() { button.setVolumeLevel(rmsdB); } }); } } private static void setMicButtonState(final MicButton button, final MicButton.State state) { if (button != null) { button.post(new Runnable() { @Override public void run() { button.setState(state); } }); } } private static void setVisibility(final View view, final int visibility) { if (view != null && view.getVisibility() != View.GONE) { view.post(new Runnable() { @Override public void run() { view.setVisibility(visibility); } }); } } private void updateComboSelector(ServiceLanguageChooser slc) { Combo combo = new Combo(getContext(), slc.getCombo()); mBComboSelector.setText(combo.getLongLabel()); } private void updateServiceLanguage(SpeechRecognizer sr) { cancelOrDestroy(); mRecognizer = sr; mRecognizer.setRecognitionListener(new SpeechInputRecognitionListener()); } private void startListening(ServiceLanguageChooser slc) { setGuiState(MicButton.State.WAITING); updateServiceLanguage(slc.getSpeechRecognizer()); mRecognizer.startListening(slc.getIntent()); mListener.onStartListening(); } /** * TODO: not sure if its better to call cancel or destroy * Note that SpeechRecognizer#destroy calls cancel first. */ private void cancelOrDestroy() { if (mRecognizer != null) { mRecognizer.destroy(); mRecognizer = null; } } private class SpeechInputRecognitionListener implements RecognitionListener { @Override public void onReadyForSpeech(Bundle params) { Log.i("onReadyForSpeech: state = " + mState); setGuiState(MicButton.State.LISTENING); setText(mTvInstruction, R.string.buttonImeStop); showMessage(""); if (mBImeKeyboard != null) { setVisibility(mBImeKeyboard, View.INVISIBLE); } } @Override public void onBeginningOfSpeech() { Log.i("onBeginningOfSpeech: state = " + mState); setGuiState(MicButton.State.RECORDING); } @Override public void onEndOfSpeech() { Log.i("onEndOfSpeech: state = " + mState); // We go into the TRANSCRIBING-state only if we were in the RECORDING-state, // otherwise we ignore this event. This improves compatibility with // Google Voice Search, which calls EndOfSpeech after onResults. if (mState == MicButton.State.RECORDING) { setGuiState(MicButton.State.TRANSCRIBING); setText(mTvInstruction, R.string.statusImeTranscribing); } } /** * We process all possible SpeechRecognizer errors. Most of them * are generated by our implementation, others can be generated by the * framework, e.g. ERROR_CLIENT results from * "stopListening called with no preceding startListening". * * @param errorCode SpeechRecognizer error code */ @Override public void onError(final int errorCode) { Log.i("onError: " + errorCode); mListener.onError(errorCode); switch (errorCode) { case SpeechRecognizer.ERROR_AUDIO: setGuiInitState(R.string.errorImeResultAudioError); break; case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: setGuiInitState(R.string.errorImeResultRecognizerBusy); break; case SpeechRecognizer.ERROR_SERVER: setGuiInitState(R.string.errorImeResultServerError); break; case SpeechRecognizer.ERROR_NETWORK: setGuiInitState(R.string.errorImeResultNetworkError); break; case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: setGuiInitState(R.string.errorImeResultNetworkTimeoutError); break; case SpeechRecognizer.ERROR_CLIENT: setGuiInitState(R.string.errorImeResultClientError); break; case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: setGuiInitState(R.string.errorImeResultInsufficientPermissions); break; case SpeechRecognizer.ERROR_NO_MATCH: setGuiInitState(R.string.errorImeResultNoMatch); break; case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: setGuiInitState(R.string.errorImeResultSpeechTimeout); break; default: Log.e("This might happen in future Android versions: code " + errorCode); setGuiInitState(R.string.errorImeResultClientError); break; } } @Override public void onPartialResults(final Bundle bundle) { Log.i("onPartialResults: state = " + mState); ArrayList<String> results = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); if (results != null && !results.isEmpty()) { // This can be true only with kaldi-gstreamer-server boolean isSemiFinal = bundle.getBoolean(Extras.EXTRA_SEMI_FINAL); showMessage(lastChars(results, isSemiFinal)); if (isSemiFinal) { mListener.onFinalResult(results, bundle); } else { mListener.onPartialResult(results); } } } @Override public void onEvent(int eventType, Bundle params) { // TODO: future work: not sure how this can be generated by the service Log.i("onEvent: type = " + eventType); } @Override public void onResults(final Bundle bundle) { Log.i("onResults: state = " + mState); ArrayList<String> results = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); Log.i("onResults: results = " + results); if (results == null || results.isEmpty()) { // If we got empty results then assume that the session ended, // e.g. cancel was called. // TODO: not sure why this was needed //mListener.onFinalResult(Collections.<String>emptyList(), bundle); } else { showMessage(lastChars(results, true)); mListener.onFinalResult(results, bundle); } setGuiInitState(0); } @Override public void onRmsChanged(float rmsdB) { //Log.i("onRmsChanged"); setMicButtonVolumeLevel(mBImeStartStop, rmsdB); } @Override public void onBufferReceived(byte[] buffer) { Log.i("View: onBufferReceived: " + buffer.length); mListener.onBufferReceived(buffer); } } }