/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.voice; import java.security.InvalidParameterException; import java.util.ArrayList; import junit.framework.Assert; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.speech.RecognizerIntent; import android.support.v4.app.Fragment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.EditText; import android.widget.ImageButton; import com.timsu.astrid.R; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.utility.Constants; /** * This class handles taking voice-input and appends the text to the registered EditText-instance. * You can have multiple VoiceInputAssistants per Fragment, just use the additional constructor * to specify unique requestCodes for the RecognizerIntent (e.g. VoiceInputAssistant.VOICE_RECOGNITION_REQUEST_CODE+i). * If you have only one VoiceInputAssitant on an Fragment, just use the normal constructor. * <p> * You can query voiceinput-capabilities by calling isVoiceInputAvailable() for external checking, * but the visibility for the microphone-button specified by the constructor is handled in configureMicrophoneButton(int). * * @author Arne Jans */ @SuppressWarnings("nls") public class VoiceInputAssistant { /** requestcode for activityresult from voicerecognizer-intent */ public static final int VOICE_RECOGNITION_REQUEST_CODE = 1234; /** * This requestcode is used to differentiate between multiple microphone-buttons on a single fragment. * Use the mightier constructor to specify your own requestCode in this case for every additional use on an fragment. * If you only use one microphone-button on an fragment, you can leave it to its default, VOICE_RECOGNITION_REQUEST_CODE. */ private int requestCode = VOICE_RECOGNITION_REQUEST_CODE; private Activity activity; private final ImageButton voiceButton; private boolean append = false; private String languageModel = RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH; /** * @param languageModel the languageModel to set */ public void setLanguageModel(String languageModel) { this.languageModel = languageModel; } /** * @return the languageModel */ public String getLanguageModel() { return languageModel; } /** Sets whether voice input will append into field */ public void setAppend(boolean append) { this.append = append; } /** * Creates a new VoiceInputAssistant-instance simply for checking the availability of the * RecognizerService. This is used for Preferences-Screens that dont want to provide * a microphone-button themselves. */ public VoiceInputAssistant() { this.voiceButton = null; } /** * This constructor can be called from a widget with a voice-button calling a dummy-activity. * * @param activity dummy-activity that starts the voice-request. */ public VoiceInputAssistant(Activity activity) { this.activity = activity; this.voiceButton = null; } /** * Creates a new VoiceInputAssistance-instance for use with a specified button and textfield. * If you need more than one microphone-button on a given fragment, use the other constructor. * * @param fragment the fragment which holds the microphone-buttone and the textField to insert recognized test * @param voiceButton the microphone-Button * @param textField the textfield that should get the resulttext */ public VoiceInputAssistant(ImageButton voiceButton) { Assert.assertNotNull("A VoiceInputAssistant without a voiceButton makes no sense!", voiceButton); this.voiceButton = voiceButton; } /** * The param requestCode is used to differentiate between multiple * microphone-buttons on a single fragment. * Use the this constructor to specify your own requestCode in * this case for every additional use on a fragment. * If you only use one microphone-button on a fragment, * you can leave it to its default, VOICE_RECOGNITION_REQUEST_CODE. * * * @param fragment * @param voiceButton * @param textField * @param requestCode has to be unique in a single fragment-context, * dont use VOICE_RECOGNITION_REQUEST_CODE, this is reserved for the other constructor */ public VoiceInputAssistant(ImageButton voiceButton, int requestCode) { this(voiceButton); if (requestCode == VOICE_RECOGNITION_REQUEST_CODE) throw new InvalidParameterException("You have to specify a unique requestCode for this VoiceInputAssistant!"); this.requestCode = requestCode; } /** * Creates a new VoiceInputAssistance-instance for use with a specified button and textfield. * If you need more than one microphone-button on a given fragment, use the other constructor. * * @param activity the activity which holds the microphone-buttone and the textField to insert recognized test * @param voiceButton the microphone-Button * @param textField the textfield that should get the resulttext */ public VoiceInputAssistant(Activity activity, ImageButton voiceButton) { Assert.assertNotNull("Each VoiceInputAssistant must be bound to a activity!", activity); Assert.assertNotNull("A VoiceInputAssistant without a voiceButton makes no sense!", voiceButton); this.activity = activity; this.voiceButton = voiceButton; } /** * The param requestCode is used to differentiate between multiple * microphone-buttons on a single fragment. * Use the this constructor to specify your own requestCode in * this case for every additional use on a activity. * If you only use one microphone-button on a activity, * you can leave it to its default, VOICE_RECOGNITION_REQUEST_CODE. * * * @param activity * @param voiceButton * @param textField * @param requestCode has to be unique in a single fragment-context, * dont use VOICE_RECOGNITION_REQUEST_CODE, this is reserved for the other constructor */ public VoiceInputAssistant(Activity activity, ImageButton voiceButton, int requestCode) { this(activity, voiceButton); if (requestCode == VOICE_RECOGNITION_REQUEST_CODE) throw new InvalidParameterException("You have to specify a unique requestCode for this VoiceInputAssistant!"); this.requestCode = requestCode; } /** * Fire an intent to start the speech recognition activity. * This is fired by the listener on the microphone-button. * * @param prompt Specify the R.string.string_id resource for the prompt-text during voice-recognition here */ public void startVoiceRecognitionActivity(Fragment fragment, int prompt) { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1); intent.putExtra(RecognizerIntent.EXTRA_PROMPT, ContextManager.getContext().getString(prompt)); String detailMessage = "Error! No Fragment or Activity was registered to handle this voiceinput-request!"; if (activity != null) activity.startActivityForResult(intent, requestCode); else if (fragment != null) fragment.startActivityForResult(intent, requestCode); else Log.e("Astrid VoiceInputAssistant", detailMessage, new IllegalStateException(detailMessage)); } /** * This callback-method has to be called from Fragment.onActivityResult within your fragment * with parameters directly on passthru.<br> * You can check in your fragment if it was really a RecognizerIntent that was handled here, * if so, this method returns true. In this case, you should call super.onActivityResult in your * fragment.onActivityResult. * <p> * If this method returns false, then it wasnt a request with a RecognizerIntent, so you can handle * these other requests as you need. * * @param activityRequestCode if this equals the requestCode specified by constructor, then results of voice-recognition * @param resultCode * @param data * @return */ public boolean handleActivityResult(int activityRequestCode, int resultCode, Intent data, EditText textField) { boolean result = false; // handle the result of voice recognition, put it into the textfield if (activityRequestCode == this.requestCode) { // this was handled here, even if voicerecognition fails for any reason // so your program flow wont get chaotic if you dont explicitly state // your own requestCodes. result = true; if (resultCode == Activity.RESULT_OK) { // Fill the quickAddBox-view with the string the recognizer thought it could have heard ArrayList<String> match = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS); // make sure we only do this if there is SomeThing (tm) returned if (match != null && match.size() > 0 && match.get(0).length() > 0) { String recognizedSpeech = match.get(0); recognizedSpeech = recognizedSpeech.substring(0, 1).toUpperCase() + recognizedSpeech.substring(1).toLowerCase(); if(append) textField.setText((textField.getText() + " " + recognizedSpeech).trim()); else textField.setText(recognizedSpeech); } } } return result; } /** * Can also be called from Fragment.onActivityResult to simply get the string result * of the speech to text, or null if it couldn't be processed. Convenient when you * don't have a bunch of UI elements to hook into. * @param activityRequestCode * @param resultCode * @param data * @return */ public String getActivityResult(int activityRequestCode, int resultCode, Intent data) { if (activityRequestCode == this.requestCode) { if (resultCode == Activity.RESULT_OK) { // Fill the quickAddBox-view with the string the recognizer thought it could have heard ArrayList<String> match = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS); // make sure we only do this if there is SomeThing (tm) returned if (match != null && match.size() > 0 && match.get(0).length() > 0) { String recognizedSpeech = match.get(0); recognizedSpeech = recognizedSpeech.substring(0, 1).toUpperCase() + recognizedSpeech.substring(1).toLowerCase(); return recognizedSpeech; } } } return null; } public void configureMicrophoneButton(final Fragment fragment, final int prompt) { if (Preferences.getBoolean(R.string.p_voiceInputEnabled, true) && VoiceRecognizer.voiceInputAvailable(ContextManager.getContext())) { voiceButton.setVisibility(View.VISIBLE); voiceButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { startVoiceRecognitionActivity(fragment, prompt); } }); } else { voiceButton.setVisibility(View.GONE); } } public void showVoiceInputMarketSearch(DialogInterface.OnClickListener onFail) { String packageName; if(AndroidUtilities.getSdkVersion() <= 7) packageName = "com.google.android.voicesearch.x"; else packageName = "com.google.android.voicesearch"; // User wants to install voice search, take them to the market Intent marketIntent = Constants.MARKET_STRATEGY.generateMarketLink(packageName); if (activity != null) { try { if (marketIntent == null) throw new ActivityNotFoundException("No market link supplied"); //$NON-NLS-1$ activity.startActivity(marketIntent); } catch (ActivityNotFoundException ane) { DialogUtilities.okDialog(activity, activity.getString(R.string.EPr_marketUnavailable_dlg), onFail); } } } }