diff options
Diffstat (limited to 'java/src/com/android/inputmethod/voice')
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; + } +} |