aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/voice
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/voice')
-rw-r--r--java/src/com/android/inputmethod/voice/EditingUtil.java162
-rw-r--r--java/src/com/android/inputmethod/voice/FieldContext.java102
-rw-r--r--java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java28
-rw-r--r--java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java21
-rw-r--r--java/src/com/android/inputmethod/voice/RecognitionView.java324
-rw-r--r--java/src/com/android/inputmethod/voice/SettingsUtil.java113
-rw-r--r--java/src/com/android/inputmethod/voice/VoiceInput.java508
-rw-r--r--java/src/com/android/inputmethod/voice/VoiceInputLogger.java177
-rw-r--r--java/src/com/android/inputmethod/voice/WaveformImage.java90
-rw-r--r--java/src/com/android/inputmethod/voice/Whitelist.java67
10 files changed, 1592 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/voice/EditingUtil.java b/java/src/com/android/inputmethod/voice/EditingUtil.java
new file mode 100644
index 000000000..6316d8ccf
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/EditingUtil.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Utility methods to deal with editing text through an InputConnection.
+ */
+public class EditingUtil {
+ private EditingUtil() {};
+
+ /**
+ * Append newText to the text field represented by connection.
+ * The new text becomes selected.
+ */
+ public static void appendText(InputConnection connection, String newText) {
+ if (connection == null) {
+ return;
+ }
+
+ // Commit the composing text
+ connection.finishComposingText();
+
+ // Add a space if the field already has text.
+ CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
+ if (charBeforeCursor != null
+ && !charBeforeCursor.equals(" ")
+ && (charBeforeCursor.length() > 0)) {
+ newText = " " + newText;
+ }
+
+ connection.setComposingText(newText, 1);
+ }
+
+ private static int getCursorPosition(InputConnection connection) {
+ ExtractedText extracted = connection.getExtractedText(
+ new ExtractedTextRequest(), 0);
+ if (extracted == null) {
+ return -1;
+ }
+ return extracted.startOffset + extracted.selectionStart;
+ }
+
+ private static int getSelectionEnd(InputConnection connection) {
+ ExtractedText extracted = connection.getExtractedText(
+ new ExtractedTextRequest(), 0);
+ if (extracted == null) {
+ return -1;
+ }
+ return extracted.startOffset + extracted.selectionEnd;
+ }
+
+ /**
+ * @param connection connection to the current text field.
+ * @param sep characters which may separate words
+ * @return the word that surrounds the cursor, including up to one trailing
+ * separator. For example, if the field contains "he|llo world", where |
+ * represents the cursor, then "hello " will be returned.
+ */
+ public static String getWordAtCursor(
+ InputConnection connection, String separators) {
+ Range range = getWordRangeAtCursor(connection, separators);
+ return (range == null) ? null : range.word;
+ }
+
+ /**
+ * Removes the word surrounding the cursor. Parameters are identical to
+ * getWordAtCursor.
+ */
+ public static void deleteWordAtCursor(
+ InputConnection connection, String separators) {
+
+ Range range = getWordRangeAtCursor(connection, separators);
+ if (range == null) return;
+
+ connection.finishComposingText();
+ // Move cursor to beginning of word, to avoid crash when cursor is outside
+ // of valid range after deleting text.
+ int newCursor = getCursorPosition(connection) - range.charsBefore;
+ connection.setSelection(newCursor, newCursor);
+ connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
+ }
+
+ /**
+ * Represents a range of text, relative to the current cursor position.
+ */
+ private static class Range {
+ /** Characters before selection start */
+ int charsBefore;
+
+ /**
+ * Characters after selection start, including one trailing word
+ * separator.
+ */
+ int charsAfter;
+
+ /** The actual characters that make up a word */
+ String word;
+
+ public Range(int charsBefore, int charsAfter, String word) {
+ if (charsBefore < 0 || charsAfter < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ this.charsBefore = charsBefore;
+ this.charsAfter = charsAfter;
+ this.word = word;
+ }
+ }
+
+ private static Range getWordRangeAtCursor(
+ InputConnection connection, String sep) {
+ if (connection == null || sep == null) {
+ return null;
+ }
+ CharSequence before = connection.getTextBeforeCursor(1000, 0);
+ CharSequence after = connection.getTextAfterCursor(1000, 0);
+ if (before == null || after == null) {
+ return null;
+ }
+
+ // Find first word separator before the cursor
+ int start = before.length();
+ while (--start > 0 && !isWhitespace(before.charAt(start - 1), sep));
+
+ // Find last word separator after the cursor
+ int end = -1;
+ while (++end < after.length() && !isWhitespace(after.charAt(end), sep));
+ if (end < after.length() - 1) {
+ end++; // Include trailing space, if it exists, in word
+ }
+
+ int cursor = getCursorPosition(connection);
+ if (start >= 0 && cursor + end <= after.length() + before.length()) {
+ String word = before.toString().substring(start, before.length())
+ + after.toString().substring(0, end);
+ return new Range(before.length() - start, end, word);
+ }
+
+ return null;
+ }
+
+ private static boolean isWhitespace(int code, String whitespace) {
+ return whitespace.contains(String.valueOf((char) code));
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/FieldContext.java b/java/src/com/android/inputmethod/voice/FieldContext.java
new file mode 100644
index 000000000..5fbacfb6c
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/FieldContext.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * Represents information about a given text field, which can be passed
+ * to the speech recognizer as context information.
+ */
+public class FieldContext {
+ private static final boolean DBG = false;
+
+ static final String LABEL = "label";
+ static final String HINT = "hint";
+ static final String PACKAGE_NAME = "packageName";
+ static final String FIELD_ID = "fieldId";
+ static final String FIELD_NAME = "fieldName";
+ static final String SINGLE_LINE = "singleLine";
+ static final String INPUT_TYPE = "inputType";
+ static final String IME_OPTIONS = "imeOptions";
+ static final String SELECTED_LANGUAGE = "selectedLanguage";
+ static final String ENABLED_LANGUAGES = "enabledLanguages";
+
+ Bundle mFieldInfo;
+
+ public FieldContext(InputConnection conn, EditorInfo info,
+ String selectedLanguage, String[] enabledLanguages) {
+ mFieldInfo = new Bundle();
+ addEditorInfoToBundle(info, mFieldInfo);
+ addInputConnectionToBundle(conn, mFieldInfo);
+ addLanguageInfoToBundle(selectedLanguage, enabledLanguages, mFieldInfo);
+ if (DBG) Log.i("FieldContext", "Bundle = " + mFieldInfo.toString());
+ }
+
+ private static String safeToString(Object o) {
+ if (o == null) {
+ return "";
+ }
+ return o.toString();
+ }
+
+ private static void addEditorInfoToBundle(EditorInfo info, Bundle bundle) {
+ if (info == null) {
+ return;
+ }
+
+ bundle.putString(LABEL, safeToString(info.label));
+ bundle.putString(HINT, safeToString(info.hintText));
+ bundle.putString(PACKAGE_NAME, safeToString(info.packageName));
+ bundle.putInt(FIELD_ID, info.fieldId);
+ bundle.putString(FIELD_NAME, safeToString(info.fieldName));
+ bundle.putInt(INPUT_TYPE, info.inputType);
+ bundle.putInt(IME_OPTIONS, info.imeOptions);
+ }
+
+ private static void addInputConnectionToBundle(
+ InputConnection conn, Bundle bundle) {
+ if (conn == null) {
+ return;
+ }
+
+ ExtractedText et = conn.getExtractedText(new ExtractedTextRequest(), 0);
+ if (et == null) {
+ return;
+ }
+ bundle.putBoolean(SINGLE_LINE, (et.flags & et.FLAG_SINGLE_LINE) > 0);
+ }
+
+ private static void addLanguageInfoToBundle(
+ String selectedLanguage, String[] enabledLanguages, Bundle bundle) {
+ bundle.putString(SELECTED_LANGUAGE, selectedLanguage);
+ bundle.putStringArray(ENABLED_LANGUAGES, enabledLanguages);
+ }
+
+ public Bundle getBundle() {
+ return mFieldInfo;
+ }
+
+ public String toString() {
+ return mFieldInfo.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java
new file mode 100644
index 000000000..ccbf5b6bc
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.content.Intent;
+
+import com.android.inputmethod.latin.LatinIME;
+
+public class LatinIMEWithVoice extends LatinIME {
+ @Override
+ protected void launchSettings() {
+ launchSettings(LatinIMEWithVoiceSettings.class);
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java
new file mode 100644
index 000000000..13a58e14d
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import com.android.inputmethod.latin.LatinIMESettings;
+
+public class LatinIMEWithVoiceSettings extends LatinIMESettings {}
diff --git a/java/src/com/android/inputmethod/voice/RecognitionView.java b/java/src/com/android/inputmethod/voice/RecognitionView.java
new file mode 100644
index 000000000..1e99c3cf7
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/RecognitionView.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.inputmethod.latin.R;
+
+/**
+ * The user interface for the "Speak now" and "working" states.
+ * Displays a recognition dialog (with waveform, voice meter, etc.),
+ * plays beeps, shows errors, etc.
+ */
+public class RecognitionView {
+ private static final String TAG = "RecognitionView";
+
+ private Handler mUiHandler; // Reference to UI thread
+ private View mView;
+ private Context mContext;
+
+ private ImageView mImage;
+ private TextView mText;
+ private View mButton;
+ private TextView mButtonText;
+ private View mProgress;
+
+ private Drawable mInitializing;
+ private Drawable mError;
+ private List<Drawable> mSpeakNow;
+
+ private float mVolume = 0.0f;
+ private int mLevel = 0;
+
+ private enum State {LISTENING, WORKING, READY}
+ private State mState = State.READY;
+
+ private float mMinMicrophoneLevel;
+ private float mMaxMicrophoneLevel;
+
+ /** Updates the microphone icon to show user their volume.*/
+ private Runnable mUpdateVolumeRunnable = new Runnable() {
+ public void run() {
+ if (mState != State.LISTENING) {
+ return;
+ }
+
+ final float min = mMinMicrophoneLevel;
+ final float max = mMaxMicrophoneLevel;
+ final int maxLevel = mSpeakNow.size() - 1;
+
+ int index = (int) ((mVolume - min) / (max - min) * maxLevel);
+ final int level = Math.min(Math.max(0, index), maxLevel);
+
+ if (level != mLevel) {
+ mImage.setImageDrawable(mSpeakNow.get(level));
+ mLevel = level;
+ }
+ mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
+ }
+ };
+
+ public RecognitionView(Context context, OnClickListener clickListener) {
+ mUiHandler = new Handler();
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mView = inflater.inflate(R.layout.recognition_status, null);
+ ContentResolver cr = context.getContentResolver();
+ mMinMicrophoneLevel = SettingsUtil.getSettingsFloat(
+ cr, SettingsUtil.LATIN_IME_MIN_MICROPHONE_LEVEL, 15.f);
+ mMaxMicrophoneLevel = SettingsUtil.getSettingsFloat(
+ cr, SettingsUtil.LATIN_IME_MAX_MICROPHONE_LEVEL, 30.f);
+
+ // Pre-load volume level images
+ Resources r = context.getResources();
+
+ mSpeakNow = new ArrayList<Drawable>();
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level0));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level1));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level2));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level3));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level4));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level5));
+ mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level6));
+
+ mInitializing = r.getDrawable(R.drawable.mic_slash);
+ mError = r.getDrawable(R.drawable.caution);
+
+ mImage = (ImageView) mView.findViewById(R.id.image);
+ mButton = mView.findViewById(R.id.button);
+ mButton.setOnClickListener(clickListener);
+ mText = (TextView) mView.findViewById(R.id.text);
+ mButtonText = (TextView) mView.findViewById(R.id.button_text);
+ mProgress = mView.findViewById(R.id.progress);
+
+ mContext = context;
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ public void restoreState() {
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ // Restart the spinner
+ if (mState == State.WORKING) {
+ ((ProgressBar)mProgress).setIndeterminate(false);
+ ((ProgressBar)mProgress).setIndeterminate(true);
+ }
+ }
+ });
+ }
+
+ public void showInitializing() {
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ prepareDialog(false, mContext.getText(R.string.voice_initializing), mInitializing,
+ mContext.getText(R.string.cancel));
+ }
+ });
+ }
+
+ public void showListening() {
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ mState = State.LISTENING;
+ prepareDialog(false, mContext.getText(R.string.voice_listening), mSpeakNow.get(0),
+ mContext.getText(R.string.cancel));
+ }
+ });
+ mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
+ }
+
+ public void updateVoiceMeter(final float rmsdB) {
+ mVolume = rmsdB;
+ }
+
+ public void showError(final String message) {
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ mState = State.READY;
+ prepareDialog(false, message, mError, mContext.getText(R.string.ok));
+ }
+ });
+ }
+
+ public void showWorking(
+ final ByteArrayOutputStream waveBuffer,
+ final int speechStartPosition,
+ final int speechEndPosition) {
+
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ mState = State.WORKING;
+ prepareDialog(true, mContext.getText(R.string.voice_working), null, mContext
+ .getText(R.string.cancel));
+ final ShortBuffer buf = ByteBuffer.wrap(waveBuffer.toByteArray()).order(
+ ByteOrder.nativeOrder()).asShortBuffer();
+ buf.position(0);
+ waveBuffer.reset();
+ showWave(buf, speechStartPosition / 2, speechEndPosition / 2);
+ }
+ });
+ }
+
+ private void prepareDialog(boolean spinVisible, CharSequence text, Drawable image,
+ CharSequence btnTxt) {
+ if (spinVisible) {
+ mProgress.setVisibility(View.VISIBLE);
+ mImage.setVisibility(View.GONE);
+ } else {
+ mProgress.setVisibility(View.GONE);
+ mImage.setImageDrawable(image);
+ mImage.setVisibility(View.VISIBLE);
+ }
+ mText.setText(text);
+ mButtonText.setText(btnTxt);
+ }
+
+ /**
+ * @return an average abs of the specified buffer.
+ */
+ private static int getAverageAbs(ShortBuffer buffer, int start, int i, int npw) {
+ int from = start + i * npw;
+ int end = from + npw;
+ int total = 0;
+ for (int x = from; x < end; x++) {
+ total += Math.abs(buffer.get(x));
+ }
+ return total / npw;
+ }
+
+
+ /**
+ * Shows waveform of input audio.
+ *
+ * Copied from version in VoiceSearch's RecognitionActivity.
+ *
+ * TODO: adjust stroke width based on the size of data.
+ * TODO: use dip rather than pixels.
+ */
+ private void showWave(ShortBuffer waveBuffer, int startPosition, int endPosition) {
+ final int w = ((View) mImage.getParent()).getWidth();
+ final int h = mImage.getHeight();
+ if (w <= 0 || h <= 0) {
+ // view is not visible this time. Skip drawing.
+ return;
+ }
+ final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+ final Paint paint = new Paint();
+ paint.setColor(0xFFFFFFFF); // 0xAARRGGBB
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setAlpha(0x90);
+
+ final PathEffect effect = new CornerPathEffect(3);
+ paint.setPathEffect(effect);
+
+ final int numSamples = waveBuffer.remaining();
+ int endIndex;
+ if (endPosition == 0) {
+ endIndex = numSamples;
+ } else {
+ endIndex = Math.min(endPosition, numSamples);
+ }
+
+ int startIndex = startPosition - 2000; // include 250ms before speech
+ if (startIndex < 0) {
+ startIndex = 0;
+ }
+ final int numSamplePerWave = 200; // 8KHz 25ms = 200 samples
+ final float scale = 10.0f / 65536.0f;
+
+ final int count = (endIndex - startIndex) / numSamplePerWave;
+ final float deltaX = 1.0f * w / count;
+ int yMax = h / 2 - 10;
+ Path path = new Path();
+ c.translate(0, yMax);
+ float x = 0;
+ path.moveTo(x, 0);
+ yMax -= 10;
+ for (int i = 0; i < count; i++) {
+ final int avabs = getAverageAbs(waveBuffer, startIndex, i , numSamplePerWave);
+ int sign = ( (i & 01) == 0) ? -1 : 1;
+ final float y = Math.min(yMax, avabs * h * scale) * sign;
+ path.lineTo(x, y);
+ x += deltaX;
+ path.lineTo(x, y);
+ }
+ if (deltaX > 4) {
+ paint.setStrokeWidth(3);
+ } else {
+ paint.setStrokeWidth(Math.max(1, (int) (deltaX -.05)));
+ }
+ c.drawPath(path, paint);
+ mImage.setImageBitmap(b);
+ mImage.setVisibility(View.VISIBLE);
+ MarginLayoutParams mProgressParams = (MarginLayoutParams)mProgress.getLayoutParams();
+ mProgressParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ -h / 2 - 18, mContext.getResources().getDisplayMetrics());
+
+ // Tweak the padding manually to fill out the whole view horizontally.
+ // TODO: Do this in the xml layout instead.
+ ((View) mImage.getParent()).setPadding(4, ((View) mImage.getParent()).getPaddingTop(), 3,
+ ((View) mImage.getParent()).getPaddingBottom());
+ mProgress.setLayoutParams(mProgressParams);
+ }
+
+
+ public void finish() {
+ mUiHandler.post(new Runnable() {
+ public void run() {
+ mState = State.READY;
+ exitWorking();
+ }
+ });
+ }
+
+ private void exitWorking() {
+ mProgress.setVisibility(View.GONE);
+ mImage.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/SettingsUtil.java b/java/src/com/android/inputmethod/voice/SettingsUtil.java
new file mode 100644
index 000000000..abf52047f
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/SettingsUtil.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Settings;
+import android.util.Log;
+
+/**
+ * Utility for retrieving settings from Settings.Secure.
+ */
+public class SettingsUtil {
+ /**
+ * A whitespace-separated list of supported locales for voice input from the keyboard.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES =
+ "latin_ime_voice_input_supported_locales";
+
+ /**
+ * A whitespace-separated list of recommended app packages for voice input from the
+ * keyboard.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES =
+ "latin_ime_voice_input_recommended_packages";
+
+ /**
+ * The maximum number of unique days to show the swipe hint for voice input.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS =
+ "latin_ime_voice_input_swipe_hint_max_days";
+
+ /**
+ * The maximum number of times to show the punctuation hint for voice input.
+ */
+ public static final String LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS =
+ "latin_ime_voice_input_punctuation_hint_max_displays";
+
+ /**
+ * Endpointer parameters for voice input from the keyboard.
+ */
+ public static final String LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS =
+ "latin_ime_speech_minimum_length_millis";
+ public static final String LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "latin_ime_speech_input_complete_silence_length_millis";
+ public static final String LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "latin_ime_speech_input_possibly_complete_silence_length_millis";
+
+ /**
+ * Min and max volume levels that can be displayed on the "speak now" screen.
+ */
+ public static final String LATIN_IME_MIN_MICROPHONE_LEVEL =
+ "latin_ime_min_microphone_level";
+ public static final String LATIN_IME_MAX_MICROPHONE_LEVEL =
+ "latin_ime_max_microphone_level";
+
+ /**
+ * The number of sentence-level alternates to request of the server.
+ */
+ public static final String LATIN_IME_MAX_VOICE_RESULTS = "latin_ime_max_voice_results";
+
+ /**
+ * Get a string-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if none can be found
+ * @return The value of the setting, or defaultValue if it couldn't be found
+ */
+ public static String getSettingsString(ContentResolver cr, String key, String defaultValue) {
+ String result = Settings.Secure.getString(cr, key);
+ return (result == null) ? defaultValue : result;
+ }
+
+ /**
+ * Get an int-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if the setting couldn't be found or parsed
+ * @return The value of the setting, or defaultValue if it couldn't be found or parsed
+ */
+ public static int getSettingsInt(ContentResolver cr, String key, int defaultValue) {
+ return Settings.Secure.getInt(cr, key, defaultValue);
+ }
+
+ /**
+ * Get a float-valued setting.
+ *
+ * @param cr The content resolver to use
+ * @param key The setting to look up
+ * @param defaultValue The default value to use if the setting couldn't be found or parsed
+ * @return The value of the setting, or defaultValue if it couldn't be found or parsed
+ */
+ public static float getSettingsFloat(ContentResolver cr, String key, float defaultValue) {
+ return Settings.Secure.getFloat(cr, key, defaultValue);
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/VoiceInput.java b/java/src/com/android/inputmethod/voice/VoiceInput.java
new file mode 100644
index 000000000..e881856dd
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/VoiceInput.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import com.android.inputmethod.latin.R;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.speech.RecognitionListener;
+import android.speech.RecognitionManager;
+import android.speech.RecognizerIntent;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Speech recognition input, including both user interface and a background
+ * process to stream audio to the network recognizer. This class supplies a
+ * View (getView()), which it updates as recognition occurs. The user of this
+ * class is responsible for making the view visible to the user, as well as
+ * handling various events returned through UiListener.
+ */
+public class VoiceInput implements OnClickListener {
+ private static final String TAG = "VoiceInput";
+ private static final String EXTRA_RECOGNITION_CONTEXT =
+ "android.speech.extras.RECOGNITION_CONTEXT";
+ private static final String EXTRA_CALLING_PACKAGE = "calling_package";
+
+ private static final String DEFAULT_RECOMMENDED_PACKAGES =
+ "com.android.mms " +
+ "com.google.android.gm " +
+ "com.google.android.talk " +
+ "com.google.android.apps.googlevoice " +
+ "com.android.email " +
+ "com.android.browser ";
+
+ // WARNING! Before enabling this, fix the problem with calling getExtractedText() in
+ // landscape view. It causes Extracted text updates to be rejected due to a token mismatch
+ public static boolean ENABLE_WORD_CORRECTIONS = false;
+
+ // Dummy word suggestion which means "delete current word"
+ public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
+
+ private Whitelist mRecommendedList;
+ private Whitelist mBlacklist;
+
+ private VoiceInputLogger mLogger;
+
+ // Names of a few intent extras defined in VoiceSearch's RecognitionService.
+ // These let us tweak the endpointer parameters.
+ private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS";
+ private static final String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS";
+ private static final String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
+ "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS";
+
+ // The usual endpointer default value for input complete silence length is 0.5 seconds,
+ // but that's used for things like voice search. For dictation-like voice input like this,
+ // we go with a more liberal value of 1 second. This value will only be used if a value
+ // is not provided from Gservices.
+ private static final String INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS = "1000";
+
+ // Used to record part of that state for logging purposes.
+ public static final int DEFAULT = 0;
+ public static final int LISTENING = 1;
+ public static final int WORKING = 2;
+ public static final int ERROR = 3;
+
+ private int mState = DEFAULT;
+
+ private final static int MSG_CLOSE_ERROR_DIALOG = 1;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_CLOSE_ERROR_DIALOG) {
+ mState = DEFAULT;
+ mRecognitionView.finish();
+ mUiListener.onCancelVoice();
+ }
+ }
+ };
+
+ /**
+ * Events relating to the recognition UI. You must implement these.
+ */
+ public interface UiListener {
+
+ /**
+ * @param recognitionResults a set of transcripts for what the user
+ * spoke, sorted by likelihood.
+ */
+ public void onVoiceResults(
+ List<String> recognitionResults,
+ Map<String, List<CharSequence>> alternatives);
+
+ /**
+ * Called when the user cancels speech recognition.
+ */
+ public void onCancelVoice();
+ }
+
+ private RecognitionManager mRecognitionManager;
+ private RecognitionListener mRecognitionListener;
+ private RecognitionView mRecognitionView;
+ private UiListener mUiListener;
+ private Context mContext;
+
+ /**
+ * @param context the service or activity in which we're running.
+ * @param uiHandler object to receive events from VoiceInput.
+ */
+ public VoiceInput(Context context, UiListener uiHandler) {
+ mLogger = VoiceInputLogger.getLogger(context);
+ mRecognitionListener = new ImeRecognitionListener();
+ mRecognitionManager = RecognitionManager.createRecognitionManager(context);
+ mRecognitionManager.setRecognitionListener(mRecognitionListener);
+ mUiListener = uiHandler;
+ mContext = context;
+ newView();
+
+ String recommendedPackages = SettingsUtil.getSettingsString(
+ context.getContentResolver(),
+ SettingsUtil.LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES,
+ DEFAULT_RECOMMENDED_PACKAGES);
+
+ mRecommendedList = new Whitelist();
+ for (String recommendedPackage : recommendedPackages.split("\\s+")) {
+ mRecommendedList.addApp(recommendedPackage);
+ }
+
+ mBlacklist = new Whitelist();
+ mBlacklist.addApp("com.android.setupwizard");
+ }
+
+ /**
+ * The configuration of the IME changed and may have caused the views to be layed out
+ * again. Restore the state of the recognition view.
+ */
+ public void onConfigurationChanged() {
+ mRecognitionView.restoreState();
+ }
+
+ /**
+ * @return true if field is blacklisted for voice
+ */
+ public boolean isBlacklistedField(FieldContext context) {
+ return mBlacklist.matches(context);
+ }
+
+ /**
+ * Used to decide whether to show voice input hints for this field, etc.
+ *
+ * @return true if field is recommended for voice
+ */
+ public boolean isRecommendedField(FieldContext context) {
+ return mRecommendedList.matches(context);
+ }
+
+ /**
+ * Start listening for speech from the user. This will grab the microphone
+ * and start updating the view provided by getView(). It is the caller's
+ * responsibility to ensure that the view is visible to the user at this stage.
+ *
+ * @param context the same FieldContext supplied to voiceIsEnabled()
+ * @param swipe whether this voice input was started by swipe, for logging purposes
+ */
+ public void startListening(FieldContext context, boolean swipe) {
+ mState = DEFAULT;
+
+ Locale locale = Locale.getDefault();
+ String localeString = locale.getLanguage() + "-" + locale.getCountry();
+
+ mLogger.start(localeString, swipe);
+
+ mState = LISTENING;
+
+ mRecognitionView.showInitializing();
+ startListeningAfterInitialization(context);
+ }
+
+ /**
+ * Called only when the recognition manager's initialization completed
+ *
+ * @param context context with which {@link #startListening(FieldContext, boolean)} was executed
+ */
+ private void startListeningAfterInitialization(FieldContext context) {
+ Intent intent = makeIntent();
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
+ intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
+ intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
+ SettingsUtil.getSettingsInt(
+ mContext.getContentResolver(),
+ SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
+ 1));
+
+ // Get endpointer params from Gservices.
+ // TODO: Consider caching these values for improved performance on slower devices.
+ final ContentResolver cr = mContext.getContentResolver();
+ putEndpointerExtra(
+ cr,
+ intent,
+ SettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
+ EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
+ null /* rely on endpointer default */);
+ putEndpointerExtra(
+ cr,
+ intent,
+ SettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
+ EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
+ INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
+ /* our default value is different from the endpointer's */);
+ putEndpointerExtra(
+ cr,
+ intent,
+ SettingsUtil.
+ LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
+ EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
+ null /* rely on endpointer default */);
+
+ mRecognitionManager.startListening(intent);
+ }
+
+ /**
+ * Gets the value of the provided Gservices key, attempts to parse it into a long,
+ * and if successful, puts the long value as an extra in the provided intent.
+ */
+ private void putEndpointerExtra(ContentResolver cr, Intent i,
+ String gservicesKey, String intentExtraKey, String defaultValue) {
+ long l = -1;
+ String s = SettingsUtil.getSettingsString(cr, gservicesKey, defaultValue);
+ if (s != null) {
+ try {
+ l = Long.valueOf(s);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "could not parse value for " + gservicesKey + ": " + s);
+ }
+ }
+
+ if (l != -1) i.putExtra(intentExtraKey, l);
+ }
+
+ public void destroy() {
+ mRecognitionManager.destroy();
+ }
+
+ /**
+ * Creates a new instance of the view that is returned by {@link #getView()}
+ * Clients should use this when a previously returned view is stuck in a
+ * layout that is being thrown away and a new one is need to show to the
+ * user.
+ */
+ public void newView() {
+ mRecognitionView = new RecognitionView(mContext, this);
+ }
+
+ /**
+ * @return a view that shows the recognition flow--e.g., "Speak now" and
+ * "working" dialogs.
+ */
+ public View getView() {
+ return mRecognitionView.getView();
+ }
+
+ /**
+ * Handle the cancel button.
+ */
+ public void onClick(View view) {
+ switch(view.getId()) {
+ case R.id.button:
+ cancel();
+ break;
+ }
+ }
+
+ public void logTextModified() {
+ mLogger.textModified();
+ }
+
+ public void logKeyboardWarningDialogShown() {
+ mLogger.keyboardWarningDialogShown();
+ }
+
+ public void logKeyboardWarningDialogDismissed() {
+ mLogger.keyboardWarningDialogDismissed();
+ }
+
+ public void logKeyboardWarningDialogOk() {
+ mLogger.keyboardWarningDialogOk();
+ }
+
+ public void logKeyboardWarningDialogCancel() {
+ mLogger.keyboardWarningDialogCancel();
+ }
+
+ public void logSwipeHintDisplayed() {
+ mLogger.swipeHintDisplayed();
+ }
+
+ public void logPunctuationHintDisplayed() {
+ mLogger.punctuationHintDisplayed();
+ }
+
+ public void logVoiceInputDelivered() {
+ mLogger.voiceInputDelivered();
+ }
+
+ public void logNBestChoose(int index) {
+ mLogger.nBestChoose(index);
+ }
+
+ public void logInputEnded() {
+ mLogger.inputEnded();
+ }
+
+ public void flushLogs() {
+ mLogger.flush();
+ }
+
+ private static Intent makeIntent() {
+ Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+
+ // On Cupcake, use VoiceIMEHelper since VoiceSearch doesn't support.
+ // On Donut, always use VoiceSearch, since VoiceIMEHelper and
+ // VoiceSearch may conflict.
+ if (Build.VERSION.RELEASE.equals("1.5")) {
+ intent = intent.setClassName(
+ "com.google.android.voiceservice",
+ "com.google.android.voiceservice.IMERecognitionService");
+ } else {
+ intent = intent.setClassName(
+ "com.google.android.voicesearch",
+ "com.google.android.voicesearch.RecognitionService");
+ }
+
+ return intent;
+ }
+
+ /**
+ * Cancel in-progress speech recognition.
+ */
+ public void cancel() {
+ switch (mState) {
+ case LISTENING:
+ mLogger.cancelDuringListening();
+ break;
+ case WORKING:
+ mLogger.cancelDuringWorking();
+ break;
+ case ERROR:
+ mLogger.cancelDuringError();
+ break;
+ }
+ mState = DEFAULT;
+
+ // Remove all pending tasks (e.g., timers to cancel voice input)
+ mHandler.removeMessages(MSG_CLOSE_ERROR_DIALOG);
+
+ mRecognitionManager.cancel();
+ mUiListener.onCancelVoice();
+ mRecognitionView.finish();
+ }
+
+ private int getErrorStringId(int errorType, boolean endpointed) {
+ switch (errorType) {
+ // We use CLIENT_ERROR to signify that voice search is not available on the device.
+ case RecognitionManager.ERROR_CLIENT:
+ return R.string.voice_not_installed;
+ case RecognitionManager.ERROR_NETWORK:
+ return R.string.voice_network_error;
+ case RecognitionManager.ERROR_NETWORK_TIMEOUT:
+ return endpointed ?
+ R.string.voice_network_error : R.string.voice_too_much_speech;
+ case RecognitionManager.ERROR_AUDIO:
+ return R.string.voice_audio_error;
+ case RecognitionManager.ERROR_SERVER:
+ return R.string.voice_server_error;
+ case RecognitionManager.ERROR_SPEECH_TIMEOUT:
+ return R.string.voice_speech_timeout;
+ case RecognitionManager.ERROR_NO_MATCH:
+ return R.string.voice_no_match;
+ default: return R.string.voice_error;
+ }
+ }
+
+ private void onError(int errorType, boolean endpointed) {
+ Log.i(TAG, "error " + errorType);
+ mLogger.error(errorType);
+ onError(mContext.getString(getErrorStringId(errorType, endpointed)));
+ }
+
+ private void onError(String error) {
+ mState = ERROR;
+ mRecognitionView.showError(error);
+ // Wait a couple seconds and then automatically dismiss message.
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_CLOSE_ERROR_DIALOG), 2000);
+ }
+
+ private class ImeRecognitionListener implements RecognitionListener {
+ // Waveform data
+ final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
+ int mSpeechStart;
+ private boolean mEndpointed = false;
+
+ public void onReadyForSpeech(Bundle noiseParams) {
+ mRecognitionView.showListening();
+ }
+
+ public void onBeginningOfSpeech() {
+ mEndpointed = false;
+ mSpeechStart = mWaveBuffer.size();
+ }
+
+ public void onRmsChanged(float rmsdB) {
+ mRecognitionView.updateVoiceMeter(rmsdB);
+ }
+
+ public void onBufferReceived(byte[] buf) {
+ try {
+ mWaveBuffer.write(buf);
+ } catch (IOException e) {}
+ }
+
+ public void onEndOfSpeech() {
+ mEndpointed = true;
+ mState = WORKING;
+ mRecognitionView.showWorking(mWaveBuffer, mSpeechStart, mWaveBuffer.size());
+ }
+
+ public void onError(int errorType) {
+ mState = ERROR;
+ VoiceInput.this.onError(errorType, mEndpointed);
+ }
+
+ public void onResults(Bundle resultsBundle) {
+ List<String> results = resultsBundle
+ .getStringArrayList(RecognitionManager.RESULTS_RECOGNITION);
+ mState = DEFAULT;
+
+ final Map<String, List<CharSequence>> alternatives =
+ new HashMap<String, List<CharSequence>>();
+ if (results.size() >= 2 && ENABLE_WORD_CORRECTIONS) {
+ final String[][] words = new String[results.size()][];
+ for (int i = 0; i < words.length; i++) {
+ words[i] = results.get(i).split(" ");
+ }
+
+ for (int key = 0; key < words[0].length; key++) {
+ alternatives.put(words[0][key], new ArrayList<CharSequence>());
+ for (int alt = 1; alt < words.length; alt++) {
+ int keyBegin = key * words[alt].length / words[0].length;
+ int keyEnd = (key + 1) * words[alt].length / words[0].length;
+
+ for (int i = keyBegin; i < Math.min(words[alt].length, keyEnd); i++) {
+ List<CharSequence> altList = alternatives.get(words[0][key]);
+ if (!altList.contains(words[alt][i]) && altList.size() < 6) {
+ altList.add(words[alt][i]);
+ }
+ }
+ }
+ }
+ }
+
+ if (results.size() > 5) {
+ results = results.subList(0, 5);
+ }
+ mUiListener.onVoiceResults(results, alternatives);
+ mRecognitionView.finish();
+ }
+
+ public void onPartialResults(final Bundle partialResults) {
+ // currently - do nothing
+ }
+
+ public void onEvent(int eventType, Bundle params) {
+ // do nothing - reserved for events that might be added in the future
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/VoiceInputLogger.java b/java/src/com/android/inputmethod/voice/VoiceInputLogger.java
new file mode 100644
index 000000000..659033340
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/VoiceInputLogger.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import com.android.common.speech.LoggingEvents;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Provides the logging facility for voice input events. This fires broadcasts back to
+ * the voice search app which then logs on our behalf.
+ *
+ * Note that debug console logging does not occur in this class. If you want to
+ * see console output of these logging events, there is a boolean switch to turn
+ * on on the VoiceSearch side.
+ */
+public class VoiceInputLogger {
+ private static final String TAG = VoiceInputLogger.class.getSimpleName();
+
+ private static VoiceInputLogger sVoiceInputLogger;
+
+ private final Context mContext;
+
+ // The base intent used to form all broadcast intents to the logger
+ // in VoiceSearch.
+ private final Intent mBaseIntent;
+
+ /**
+ * Returns the singleton of the logger.
+ *
+ * @param contextHint a hint context used when creating the logger instance.
+ * Ignored if the singleton instance already exists.
+ */
+ public static synchronized VoiceInputLogger getLogger(Context contextHint) {
+ if (sVoiceInputLogger == null) {
+ sVoiceInputLogger = new VoiceInputLogger(contextHint);
+ }
+ return sVoiceInputLogger;
+ }
+
+ public VoiceInputLogger(Context context) {
+ mContext = context;
+
+ mBaseIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT);
+ mBaseIntent.putExtra(LoggingEvents.EXTRA_APP_NAME, LoggingEvents.VoiceIme.APP_NAME);
+ }
+
+ private Intent newLoggingBroadcast(int event) {
+ Intent i = new Intent(mBaseIntent);
+ i.putExtra(LoggingEvents.EXTRA_EVENT, event);
+ return i;
+ }
+
+ public void flush() {
+ Intent i = new Intent(mBaseIntent);
+ i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
+ mContext.sendBroadcast(i);
+ }
+
+ public void keyboardWarningDialogShown() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_SHOWN));
+ }
+
+ public void keyboardWarningDialogDismissed() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_DISMISSED));
+ }
+
+ public void keyboardWarningDialogOk() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_OK));
+ }
+
+ public void keyboardWarningDialogCancel() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_CANCEL));
+ }
+
+ public void settingsWarningDialogShown() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_SHOWN));
+ }
+
+ public void settingsWarningDialogDismissed() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_DISMISSED));
+ }
+
+ public void settingsWarningDialogOk() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_OK));
+ }
+
+ public void settingsWarningDialogCancel() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_CANCEL));
+ }
+
+ public void swipeHintDisplayed() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.SWIPE_HINT_DISPLAYED));
+ }
+
+ public void cancelDuringListening() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_LISTENING));
+ }
+
+ public void cancelDuringWorking() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_WORKING));
+ }
+
+ public void cancelDuringError() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_ERROR));
+ }
+
+ public void punctuationHintDisplayed() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.PUNCTUATION_HINT_DISPLAYED));
+ }
+
+ public void error(int code) {
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.ERROR);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_ERROR_CODE, code);
+ mContext.sendBroadcast(i);
+ }
+
+ public void start(String locale, boolean swipe) {
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.START);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_LOCALE, locale);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_SWIPE, swipe);
+ i.putExtra(LoggingEvents.EXTRA_TIMESTAMP, System.currentTimeMillis());
+ mContext.sendBroadcast(i);
+ }
+
+ public void voiceInputDelivered() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.VOICE_INPUT_DELIVERED));
+ }
+
+ public void textModified() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED));
+ }
+
+ public void nBestChoose(int index) {
+ Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.N_BEST_CHOOSE);
+ i.putExtra(LoggingEvents.VoiceIme.EXTRA_N_BEST_CHOOSE_INDEX, index);
+ mContext.sendBroadcast(i);
+ }
+
+ public void inputEnded() {
+ mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.INPUT_ENDED));
+ }
+
+ public void voiceInputSettingEnabled() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_ENABLED));
+ }
+
+ public void voiceInputSettingDisabled() {
+ mContext.sendBroadcast(newLoggingBroadcast(
+ LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_DISABLED));
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/WaveformImage.java b/java/src/com/android/inputmethod/voice/WaveformImage.java
new file mode 100644
index 000000000..08d87c8f3
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/WaveformImage.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008-2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+/**
+ * Utility class to draw a waveform into a bitmap, given a byte array
+ * that represents the waveform as a sequence of 16-bit integers.
+ * Adapted from RecognitionActivity.java.
+ */
+public class WaveformImage {
+ private static final int SAMPLING_RATE = 8000;
+
+ private WaveformImage() {}
+
+ public static Bitmap drawWaveform(
+ ByteArrayOutputStream waveBuffer, int w, int h, int start, int end) {
+ final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+ final Paint paint = new Paint();
+ paint.setColor(0xFFFFFFFF); // 0xRRGGBBAA
+ paint.setAntiAlias(true);
+ paint.setStrokeWidth(0);
+
+ final ShortBuffer buf = ByteBuffer
+ .wrap(waveBuffer.toByteArray())
+ .order(ByteOrder.nativeOrder())
+ .asShortBuffer();
+ buf.position(0);
+
+ final int numSamples = waveBuffer.size() / 2;
+ final int delay = (SAMPLING_RATE * 100 / 1000);
+ int endIndex = end / 2 + delay;
+ if (end == 0 || endIndex >= numSamples) {
+ endIndex = numSamples;
+ }
+ int index = start / 2 - delay;
+ if (index < 0) {
+ index = 0;
+ }
+ final int size = endIndex - index;
+ int numSamplePerPixel = 32;
+ int delta = size / (numSamplePerPixel * w);
+ if (delta == 0) {
+ numSamplePerPixel = size / w;
+ delta = 1;
+ }
+
+ final float scale = 3.5f / 65536.0f;
+ // do one less column to make sure we won't read past
+ // the buffer.
+ try {
+ for (int i = 0; i < w - 1 ; i++) {
+ final float x = i;
+ for (int j = 0; j < numSamplePerPixel; j++) {
+ final short s = buf.get(index);
+ final float y = (h / 2) - (s * h * scale);
+ c.drawPoint(x, y, paint);
+ index += delta;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ // this can happen, but we don't care
+ }
+
+ return b;
+ }
+}
diff --git a/java/src/com/android/inputmethod/voice/Whitelist.java b/java/src/com/android/inputmethod/voice/Whitelist.java
new file mode 100644
index 000000000..167b688ca
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/Whitelist.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.inputmethod.voice;
+
+import android.os.Bundle;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A set of text fields where speech has been explicitly enabled.
+ */
+public class Whitelist {
+ private List<Bundle> mConditions;
+
+ public Whitelist() {
+ mConditions = new ArrayList<Bundle>();
+ }
+
+ public Whitelist(List<Bundle> conditions) {
+ this.mConditions = conditions;
+ }
+
+ public void addApp(String app) {
+ Bundle bundle = new Bundle();
+ bundle.putString("packageName", app);
+ mConditions.add(bundle);
+ }
+
+ /**
+ * @return true if the field is a member of the whitelist.
+ */
+ public boolean matches(FieldContext context) {
+ for (Bundle condition : mConditions) {
+ if (matches(condition, context.getBundle())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return true of all values in condition are matched by a value
+ * in target.
+ */
+ private boolean matches(Bundle condition, Bundle target) {
+ for (String key : condition.keySet()) {
+ if (!condition.getString(key).equals(target.getString(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+}