controls =
@@ -173,6 +178,11 @@ public final class TaskEditActivity extends TabActivity {
/** edit control receiver */
private final ControlReceiver controlReceiver = new ControlReceiver();
+ /** voice assistant for notes-creation */
+ private VoiceInputAssistant voiceNoteAssistant = null;
+
+ private EditText notesEditText;
+
/* ======================================================================
* ======================================================= initialization
* ====================================================================== */
@@ -235,6 +245,15 @@ public final class TaskEditActivity extends TabActivity {
controls.add(new ImportanceControlSet(R.id.importance_container));
controls.add(new UrgencyControlSet(R.id.urgency));
+ // prepare and set listener for voice-button
+ voiceAddNoteButton = (ImageButton) findViewById(R.id.voiceAddNoteButton);
+ notesEditText = (EditText) findViewById(R.id.notes);
+ int prompt = R.string.TEA_voice_edit_note_prompt;
+ voiceNoteAssistant = new VoiceInputAssistant(this, voiceAddNoteButton,
+ notesEditText);
+ voiceNoteAssistant.setLanguageModel(RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ voiceNoteAssistant.configureMicrophoneButton(prompt);
+
new Thread() {
@Override
public void run() {
@@ -254,7 +273,7 @@ public final class TaskEditActivity extends TabActivity {
try {
if(ProducteevUtilities.INSTANCE.isLoggedIn()) {
controls.add(new ProducteevControlSet(TaskEditActivity.this, addonsAddons));
- ((TextView)findViewById(R.id.notes)).setHint(R.string.producteev_TEA_notes);
+ notesEditText.setHint(R.string.producteev_TEA_notes);
((TextView)findViewById(R.id.notes_label)).setHint(R.string.producteev_TEA_notes);
}
} catch (Exception e) {
@@ -286,7 +305,8 @@ public final class TaskEditActivity extends TabActivity {
}
});
- controls.add(new EditTextControlSet(Task.NOTES, R.id.notes));
+ notesControlSet = new EditTextControlSet(Task.NOTES, R.id.notes);
+ controls.add(notesControlSet);
controls.add( new ReminderControlSet(R.id.reminder_due,
R.id.reminder_overdue, R.id.reminder_alarm));
controls.add( new RandomReminderControlSet(R.id.reminder_random,
@@ -596,6 +616,18 @@ public final class TaskEditActivity extends TabActivity {
populateFields();
}
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // handle the result of voice recognition, put it into the appropiate textfield
+ voiceNoteAssistant.handleActivityResult(requestCode, resultCode, data);
+
+ // write the voicenote into the model, or it will be deleted by onResume.populateFields
+ // (due to the activity-change)
+ notesControlSet.writeToModel(model);
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -604,6 +636,11 @@ public final class TaskEditActivity extends TabActivity {
outState.putParcelable(TASK_IN_PROGRESS, model);
}
+ @Override
+ protected void onRestoreInstanceState(Bundle inState) {
+ super.onRestoreInstanceState(inState);
+ }
+
@Override
protected void onStart() {
super.onStart();
diff --git a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
index 65bc3312a..c86ce95e9 100644
--- a/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
+++ b/astrid/src/com/todoroo/astrid/activity/TaskListActivity.java
@@ -91,6 +91,7 @@ import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.utility.AstridPreferences;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;
+import com.todoroo.astrid.voice.VoiceInputAssistant;
import com.todoroo.astrid.widget.TasksWidget;
/**
@@ -157,6 +158,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
protected int sortFlags;
protected int sortSort;
+ private ImageButton voiceAddButton;
private ImageButton quickAddButton;
private EditText quickAddBox;
private Timer backgroundTimer;
@@ -164,6 +166,7 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
private final TaskListContextMenuExtensionLoader contextMenuExtensionLoader = new TaskListContextMenuExtensionLoader();
+ private VoiceInputAssistant voiceInputAssistant;
/* ======================================================================
* ======================================================= initialization
@@ -374,6 +377,14 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
}
});
+ // prepare and set listener for voice add button
+ voiceAddButton = (ImageButton) findViewById(R.id.voiceAddButton);
+ int prompt = R.string.TLA_voice_edit_prompt;
+ if (Preferences.getBoolean(R.string.p_voiceInputCreatesTask, false))
+ prompt = R.string.TLA_voice_add_prompt;
+ voiceInputAssistant = new VoiceInputAssistant(this,voiceAddButton,quickAddBox);
+ voiceInputAssistant.configureMicrophoneButton(prompt);
+
// set listener for extended add button
((ImageButton)findViewById(R.id.extendedAddButton)).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
@@ -438,6 +449,12 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Override
protected void onResume() {
super.onResume();
+ if (Preferences.getBoolean(R.string.p_voiceInputEnabled, true) && voiceInputAssistant.isVoiceInputAvailable()) {
+ voiceAddButton.setVisibility(View.VISIBLE);
+ } else {
+ voiceAddButton.setVisibility(View.GONE);
+ }
+
registerReceiver(detailReceiver,
new IntentFilter(AstridApiConstants.BROADCAST_SEND_DETAILS));
registerReceiver(detailReceiver,
@@ -550,6 +567,17 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // handle the result of voice recognition, put it into the textfield
+ if (voiceInputAssistant.handleActivityResult(requestCode, resultCode, data)) {
+ // if user wants, create the task directly (with defaultvalues) after saying it
+ if (Preferences.getBoolean(R.string.p_voiceInputCreatesTask, false))
+ quickAddTask(quickAddBox.getText().toString(), true);
+ super.onActivityResult(requestCode, resultCode, data);
+
+ // the rest of onActivityResult is totally unrelated to voicerecognition, so bail out
+ return;
+ }
+
super.onActivityResult(requestCode, resultCode, data);
if(resultCode != RESULT_CANCELED) {
@@ -990,5 +1018,4 @@ public class TaskListActivity extends ListActivity implements OnScrollListener,
setUpTaskList();
}
-
}
diff --git a/astrid/src/com/todoroo/astrid/voice/VoiceInputAssistant.java b/astrid/src/com/todoroo/astrid/voice/VoiceInputAssistant.java
new file mode 100644
index 000000000..53524a03e
--- /dev/null
+++ b/astrid/src/com/todoroo/astrid/voice/VoiceInputAssistant.java
@@ -0,0 +1,205 @@
+package com.todoroo.astrid.voice;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.Assert;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.speech.RecognizerIntent;
+import android.text.Editable;
+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.utility.Preferences;
+
+/**
+ * This class handles taking voice-input and appends the text to the registered EditText-instance.
+ * You can have multiple VoiceInputAssistants per Activity, 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 Activity, just use the normal constructor.
+ *
+ * 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
+ */
+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 activity.
+ * Use the mightier constructor to specify your own requestCode in this case for every additional use on an activity.
+ * If you only use one microphone-button on an activity, you can leave it to its default, VOICE_RECOGNITION_REQUEST_CODE.
+ */
+ private int requestCode = VOICE_RECOGNITION_REQUEST_CODE;
+ private final Activity activity;
+ private final ImageButton voiceButton;
+ private final EditText textField;
+
+ private boolean voiceInputAvailable;
+
+ 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;
+ }
+
+ /**
+ * 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(Activity activity) {
+ Assert.assertNotNull("Each VoiceInputAssistant must be bound to an activity!", activity);
+ this.activity = activity;
+ this.voiceButton = null;
+ this.textField = 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 Activity, 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, EditText textField) {
+ Assert.assertNotNull("Each VoiceInputAssistant must be bound to an activity!", activity);
+ Assert.assertNotNull("A VoiceInputAssistant without a voiceButton makes no sense!", voiceButton);
+ Assert.assertNotNull("You have to specify a textfield that is bound to this VoiceInputAssistant!!", textField);
+ this.activity = activity;
+ this.voiceButton = voiceButton;
+ this.textField = textField;
+ }
+
+ /**
+ * The param requestCode is used to differentiate between multiple
+ * microphone-buttons on a single activity.
+ * Use the this constructor to specify your own requestCode in
+ * this case for every additional use on an activity.
+ * If you only use one microphone-button on an 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 Activity-context,
+ * dont use VOICE_RECOGNITION_REQUEST_CODE, this is reserved for the other constructor
+ */
+ public VoiceInputAssistant(Activity activity, ImageButton voiceButton, EditText textField, int requestCode) {
+ this(activity, voiceButton, textField);
+ 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(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, activity.getString(prompt));
+ activity.startActivityForResult(intent, requestCode);
+ }
+
+ /**
+ * This callback-method has to be called from Activity.onActivityResult within your activity
+ * with parameters directly on passthru.
+ * You can check in your activity 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
+ * activity.onActivityResult.
+ *
+ * If this method returns false, then it wasnt a request with a RecognizerIntent, so you can handle
+ * these other requests as you need.
+ *
+ * @param requestCode if this equals the requestCode specified by constructor, then results of voice-recognition
+ * @param resultCode
+ * @param data
+ * @return
+ */
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ boolean result = false;
+ // handle the result of voice recognition, put it into the textfield
+ if (requestCode == 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 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) {
+ Editable currentText = textField.getText();
+ String recognizedSpeech = match.get(0);
+
+ if (currentText.length() > 0) {
+ // if something is already typed in, append the recognized speech,
+ // add a space if it isn't already there
+ textField.append((currentText.toString().endsWith(" ") ? recognizedSpeech : " "+recognizedSpeech ));
+ } else {
+ textField.setText(recognizedSpeech);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Call this to see if your phone supports voiceinput in its current configuration.
+ * If this method returns false, it could also mean that Google Voicesearch is simply
+ * not installed.
+ * If this method returns true, internal use of it enables the registered microphone-button.
+ *
+ * @return whether this phone supports voiceinput
+ */
+ public boolean isVoiceInputAvailable() {
+ // Check to see if a recognition activity is present
+ PackageManager pm = activity.getPackageManager();
+ List activities = pm.queryIntentActivities(
+ new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
+ return (activities.size() != 0);
+ }
+
+ public void configureMicrophoneButton(final int prompt) {
+ if (Preferences.getBoolean(R.string.p_voiceInputEnabled, true) && isVoiceInputAvailable()) {
+ voiceButton.setVisibility(View.VISIBLE);
+ voiceButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ startVoiceRecognitionActivity(prompt);
+ }
+ });
+ } else {
+ voiceButton.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/astrid/src/com/todoroo/astrid/voice/VoiceOutputAssistant.java b/astrid/src/com/todoroo/astrid/voice/VoiceOutputAssistant.java
new file mode 100644
index 000000000..476a45728
--- /dev/null
+++ b/astrid/src/com/todoroo/astrid/voice/VoiceOutputAssistant.java
@@ -0,0 +1,146 @@
+/**
+ *
+ */
+package com.todoroo.astrid.voice;
+
+import java.util.HashMap;
+import java.util.Locale;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeech.OnInitListener;
+import android.util.Log;
+
+import com.todoroo.andlib.service.ContextManager;
+
+/**
+ * @author Arne Jans
+ *
+ */
+public class VoiceOutputAssistant implements OnInitListener {
+
+ private static final int MY_DATA_CHECK_CODE = 2534;
+ private static final String TAG = "Astrid.VoiceOutputAssistant";
+ private final Context context;
+ private static VoiceOutputAssistant instance = null;
+ private TextToSpeech mTts;
+ private boolean isTTSInitialized;
+ private boolean retryLastSpeak;
+ private String textToSpeak;
+ private static final HashMap ttsParams = new HashMap();
+
+ static {
+ ttsParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
+ String.valueOf(AudioManager.STREAM_NOTIFICATION));
+ }
+
+ private VoiceOutputAssistant() {
+ this.context = ContextManager.getContext().getApplicationContext();
+ }
+
+ public static VoiceOutputAssistant getInstance() {
+ if (instance == null) {
+ instance = new VoiceOutputAssistant();
+ }
+ return instance;
+ }
+
+ public void checkIsTTSInstalled() {
+ if (!isTTSInitialized && context instanceof Activity) {
+ Intent checkIntent = new Intent();
+ checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
+ ((Activity) context).startActivityForResult(checkIntent,
+ MY_DATA_CHECK_CODE);
+ }
+ }
+
+ public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == MY_DATA_CHECK_CODE) {
+ if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
+ // success, create the TTS instance
+ initTTS();
+ } else {
+ // missing data, install it
+ Intent installIntent = new Intent();
+ installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
+ context.startActivity(installIntent);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private void initTTS() {
+ mTts = new TextToSpeech(context, (OnInitListener)this);
+ }
+
+ public void queueSpeak(String textToSpeak) {
+ if (mTts != null && isTTSInitialized) {
+ mTts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, ttsParams);
+ while (mTts.isSpeaking()) {
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ } else {
+ retryLastSpeak = true;
+ this.textToSpeak = textToSpeak;
+ initTTS();
+ }
+ }
+
+ @Override
+ public void onInit(int status) {
+ // status can be either TextToSpeech.SUCCESS or TextToSpeech.ERROR.
+ if (status == TextToSpeech.SUCCESS) {
+ // Set preferred language to US english.
+ // Note that a language may not be available, and the result will indicate this.
+ int result = mTts.setLanguage(Locale.getDefault());
+ // Try this someday for some interesting results.
+ // int result mTts.setLanguage(Locale.FRANCE);
+ if (result == TextToSpeech.LANG_MISSING_DATA ||
+ result == TextToSpeech.LANG_NOT_SUPPORTED) {
+ // Language data is missing or the language is not supported.
+ Log.e(TAG, "Language is not available.");
+ } else {
+ // Check the documentation for other possible result codes.
+ // For example, the language may be available for the locale,
+ // but not for the specified country and variant.
+
+ mTts.speak("", 0, null);
+
+ // The TTS engine has been successfully initialized.
+ isTTSInitialized = true;
+ // if this request came from queueSpeak, then speak it and reset the memento
+ if (retryLastSpeak && this.textToSpeak != null) {
+ this.queueSpeak(this.textToSpeak);
+ retryLastSpeak = false;
+ textToSpeak = null;
+ }
+ }
+ } else {
+ // Initialization failed.
+ Log.e(TAG, "Could not initialize TextToSpeech.");
+ }
+ }
+
+ /**
+ * Has to be called in onDestroy of the activity that uses this instance of VoiceOutputAssistant.
+ */
+ public void onDestroy() {
+ if (mTts != null && isTTSInitialized) {
+ mTts.shutdown();
+ mTts = null;
+ isTTSInitialized = false;
+ }
+ }
+
+}