diff --git a/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java b/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java index 69f3f3a33..fe1df45e5 100644 --- a/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java +++ b/astrid/plugin-src/com/todoroo/astrid/reminders/Notifications.java @@ -22,11 +22,12 @@ import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.service.NotificationManager; import com.todoroo.andlib.service.NotificationManager.AndroidNotificationManager; import com.todoroo.andlib.utility.DateUtilities; +import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.service.AstridDependencyInjector; import com.todoroo.astrid.utility.Constants; -import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.voice.VoiceOutputAssistant; public class Notifications extends BroadcastReceiver { @@ -94,6 +95,8 @@ public class Notifications extends BroadcastReceiver { if(!showTaskNotification(id, type, reminder)) { notificationManager.cancel((int)id); } + // shutdown the VoiceOutputAssistant for now + VoiceOutputAssistant.getInstance().onDestroy(); } // --- notification creation @@ -217,6 +220,7 @@ public class Notifications extends BroadcastReceiver { TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); int callState = tm.getCallState(); + boolean voiceReminder = Preferences.getBoolean(R.string.p_voiceRemindersEnabled, false); // if non-stop mode is activated, set up the flags for insistent // notification, and increase the volume to full volume, so the user // will actually pay attention to the alarm @@ -225,6 +229,7 @@ public class Notifications extends BroadcastReceiver { notification.audioStreamType = AudioManager.STREAM_ALARM; audioManager.setStreamVolume(AudioManager.STREAM_ALARM, audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), 0); + voiceReminder = false; } else { notification.audioStreamType = AudioManager.STREAM_NOTIFICATION; } @@ -232,10 +237,12 @@ public class Notifications extends BroadcastReceiver { // quiet hours = no sound if(quietHours || callState != TelephonyManager.CALL_STATE_IDLE) { notification.sound = null; + voiceReminder = false; } else { String notificationPreference = Preferences.getStringValue(R.string.p_rmd_ringtone); if(audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) { notification.sound = null; + voiceReminder = false; } else if(notificationPreference != null) { if(notificationPreference.length() > 0) { Uri notificationSound = Uri.parse(notificationPreference); @@ -266,6 +273,17 @@ public class Notifications extends BroadcastReceiver { Log.w("Astrid", "Logging notification: " + text); //$NON-NLS-1$ //$NON-NLS-2$ notificationManager.notify(notificationId, notification); + if (voiceReminder) { + while (audioManager.getMode() == AudioManager.MODE_RINGTONE) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + VoiceOutputAssistant.getInstance().queueSpeak(text); + } } /** diff --git a/astrid/res/drawable/Thumbs.db b/astrid/res/drawable/Thumbs.db new file mode 100644 index 000000000..6fc9a28a1 Binary files /dev/null and b/astrid/res/drawable/Thumbs.db differ diff --git a/astrid/res/drawable/ic_btn_speak_now.png b/astrid/res/drawable/ic_btn_speak_now.png new file mode 100644 index 000000000..83ee68b5d Binary files /dev/null and b/astrid/res/drawable/ic_btn_speak_now.png differ diff --git a/astrid/res/layout/task_edit_activity.xml b/astrid/res/layout/task_edit_activity.xml index 4f5d14b91..a6447d64b 100644 --- a/astrid/res/layout/task_edit_activity.xml +++ b/astrid/res/layout/task_edit_activity.xml @@ -88,18 +88,31 @@ android:layout_height="wrap_content" android:text="@string/TEA_note_label" style="@style/TextAppearance.GEN_EditLabel" /> - - + android:layout_height="wrap_content"> + + + + + + + + Notizen zu Aufgaben anzeigen + + Spracheingabe aktivieren Notizen werden angezeigt, wenn Sie auf eine Aufgabe tippen Notizen werden immer angezeigt + + Sprachfunktionen + + Mikrofon-Button wird ausgeblendet + + Mikrofon-Button wird angezeigt + Neue Standardeinstellungen für Aufgaben diff --git a/astrid/res/values/keys.xml b/astrid/res/values/keys.xml index 45aa8f343..ca51de520 100644 --- a/astrid/res/values/keys.xml +++ b/astrid/res/values/keys.xml @@ -22,6 +22,9 @@ notification_ringtone + + notification_voice + notif_theme @@ -127,6 +130,9 @@ colorize font_size notesVisible + voiceInputEnabled + voiceInputCreatesTask + voiceRemindersEnabled diff --git a/astrid/res/values/strings-core.xml b/astrid/res/values/strings-core.xml index 405e0a374..523568fc6 100644 --- a/astrid/res/values/strings-core.xml +++ b/astrid/res/values/strings-core.xml @@ -90,6 +90,10 @@ Add to this list... + + Speak to add a task + Speak to add to tasktitle + @@ -209,6 +213,12 @@ Add-ons + + Speak to add to current title + + + Speak to add to current note + Title @@ -330,11 +340,30 @@ Show Notes In Task + + Voice-input is not installed.\nDo you want to go to the market and install it? + + Unfortunately voice-input is not available for your system.\nIf possible, please update your system to 2.1 or later. + + Unfortunately the market is not available for your system.\nIf possible, try downloading voicesearch from another source. + + Enable voice-input + + Enable direct task-creation per voice + + Enable voice-reminders Notes will be displayed when you tap a task Notes will always be displayed + + Voice + + Voice-Button will be hidden + + Voice-Button will be shown + New Task Defaults diff --git a/astrid/res/xml/preferences.xml b/astrid/res/xml/preferences.xml index 8acdef547..dbdc45d0d 100644 --- a/astrid/res/xml/preferences.xml +++ b/astrid/res/xml/preferences.xml @@ -26,4 +26,19 @@ android:defaultValue="true" /> + + + + + diff --git a/astrid/src/com/todoroo/astrid/activity/EditPreferences.java b/astrid/src/com/todoroo/astrid/activity/EditPreferences.java index 00c35aa71..464282929 100644 --- a/astrid/src/com/todoroo/astrid/activity/EditPreferences.java +++ b/astrid/src/com/todoroo/astrid/activity/EditPreferences.java @@ -11,15 +11,20 @@ import java.util.Map.Entry; import org.weloveastrid.rmilk.MilkPreferences; import org.weloveastrid.rmilk.MilkUtilities; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; +import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceCategory; import android.preference.PreferenceScreen; -import android.preference.Preference.OnPreferenceClickListener; import android.widget.Toast; import com.timsu.astrid.R; @@ -27,6 +32,7 @@ import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.sql.Criterion; +import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.andlib.widget.TodorooPreferences; @@ -38,6 +44,8 @@ import com.todoroo.astrid.service.StartupService; import com.todoroo.astrid.service.TaskService; import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Flags; +import com.todoroo.astrid.voice.VoiceInputAssistant; +import com.todoroo.astrid.voice.VoiceOutputAssistant; /** * Displays the preference screen for users to edit their preferences @@ -57,6 +65,8 @@ public class EditPreferences extends TodorooPreferences { @Autowired private Database database; + private VoiceInputAssistant voiceInputAssistant; + public EditPreferences() { DependencyInjectionService.getInstance().inject(this); } @@ -73,6 +83,7 @@ public class EditPreferences extends TodorooPreferences { ContextManager.setContext(this); PreferenceScreen screen = getPreferenceScreen(); + voiceInputAssistant = new VoiceInputAssistant(this); // load plug-ins Intent queryIntent = new Intent(AstridApiConstants.ACTION_SETTINGS); @@ -189,8 +200,8 @@ public class EditPreferences extends TodorooPreferences { } @Override - public void updatePreferences(Preference preference, Object value) { - Resources r = getResources(); + public void updatePreferences(final Preference preference, Object value) { + final Resources r = getResources(); if (r.getString(R.string.p_showNotes).equals(preference.getKey())) { if (value != null && !(Boolean)value) preference.setSummary(R.string.EPr_showNotes_desc_disabled); @@ -200,6 +211,64 @@ public class EditPreferences extends TodorooPreferences { taskService.clearDetails(Criterion.all); Flags.set(Flags.REFRESH); } + } else if (r.getString(R.string.p_voiceInputEnabled).equals(preference.getKey())) { + if (value != null && (Boolean)value) + if (!voiceInputAssistant.isVoiceInputAvailable()) { + // voicesearch available since 2.1 + if (AndroidUtilities.getSdkVersion() > 6) { + + DialogUtilities.okCancelDialog(this, + r.getString(R.string.EPr_voiceInputInstall_dlg), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + dialog.dismiss(); + // User wants to install voicesearch, take him to the market + Intent marketIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse("market://search?q=pname:" + //$NON-NLS-1$ + "com.google.android.voicesearch.x")); + try { + startActivity(marketIntent); + } catch (ActivityNotFoundException ane) { + DialogUtilities.okDialog(EditPreferences.this, + r.getString(R.string.EPr_marketUnavailable_dlg), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + ((CheckBoxPreference)preference).setChecked(false); + dialog.dismiss(); + } + }); + } + } + }, + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + ((CheckBoxPreference)preference).setChecked(false); + dialog.dismiss(); + } + }); + } else { + DialogUtilities.okDialog(this, + r.getString(R.string.EPr_voiceInputUnavailable_dlg), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + ((CheckBoxPreference)preference).setChecked(false); + dialog.dismiss(); + } + }); + } + } + } else if (r.getString(R.string.p_voiceRemindersEnabled).equals(preference.getKey())) { + if (value != null && (Boolean)value) + VoiceOutputAssistant.getInstance().checkIsTTSInstalled(); + } else if (r.getString(R.string.p_statistics).equals(preference.getKey())) { if (value != null && !(Boolean)value) @@ -209,5 +278,11 @@ public class EditPreferences extends TodorooPreferences { } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + VoiceOutputAssistant.getInstance().handleActivityResult(requestCode, resultCode, data); + super.onActivityResult(requestCode, resultCode, data); + } + } \ No newline at end of file diff --git a/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java b/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java index 375209037..a7002280e 100644 --- a/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java +++ b/astrid/src/com/todoroo/astrid/activity/TaskEditActivity.java @@ -40,6 +40,7 @@ import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; +import android.speech.RecognizerIntent; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; @@ -90,6 +91,7 @@ import com.todoroo.astrid.tags.TagsControlSet; import com.todoroo.astrid.timers.TimerControlSet; import com.todoroo.astrid.ui.DeadlineTimePickerDialog; import com.todoroo.astrid.ui.DeadlineTimePickerDialog.OnDeadlineTimeSetListener; +import com.todoroo.astrid.voice.VoiceInputAssistant; /** * This activity is responsible for creating new tasks and editing existing @@ -154,6 +156,9 @@ public final class TaskEditActivity extends TabActivity { // --- UI components + private ImageButton voiceAddNoteButton; + + private EditTextControlSet notesControlSet = null; private EditText title; private final List 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; + } + } + +}