diff options
Diffstat (limited to 'src')
27 files changed, 3891 insertions, 223 deletions
diff --git a/src/com/android/inputmethod/latin/BinaryDictionary.java b/src/com/android/inputmethod/latin/BinaryDictionary.java index 14c543514..68d8b740c 100644 --- a/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -35,6 +35,7 @@ public class BinaryDictionary extends Dictionary { private static final boolean ENABLE_MISSED_CHARACTERS = true; private int mNativeDict; + private int mDictLength; // This value is set from native code, don't change the name!!!! private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; private int[] mFrequencies = new int[MAX_WORDS]; @@ -125,8 +126,14 @@ public class BinaryDictionary extends Dictionary { return isValidWordNative(mNativeDict, chars, chars.length); } + public int getSize() { + return mDictLength; // This value is initialized on the call to openNative() + } + + @Override public synchronized void close() { if (mNativeDict != 0) { + System.err.println("Closing BinaryDictionary"); closeNative(mNativeDict); mNativeDict = 0; } diff --git a/src/com/android/inputmethod/latin/CandidateView.java b/src/com/android/inputmethod/latin/CandidateView.java index f397363c3..0b6b89e6b 100755 --- a/src/com/android/inputmethod/latin/CandidateView.java +++ b/src/com/android/inputmethod/latin/CandidateView.java @@ -26,6 +26,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.Paint.Align; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; @@ -82,7 +83,9 @@ public class CandidateView extends View { private int mDescent; private boolean mScrolled; private int mTargetScrollX; - + + private int mMinTouchableWidth; + private int mTotalWidth; private GestureDetector mGestureDetector; @@ -113,7 +116,7 @@ public class CandidateView extends View { public CandidateView(Context context, AttributeSet attrs) { super(context, attrs); mSelectionHighlight = context.getResources().getDrawable( - com.android.internal.R.drawable.list_selector_background_pressed); + R.drawable.list_selector_background_pressed); LayoutInflater inflate = (LayoutInflater) context @@ -133,13 +136,16 @@ public class CandidateView extends View { mPaint.setAntiAlias(true); mPaint.setTextSize(mPreviewText.getTextSize()); mPaint.setStrokeWidth(0); + mPaint.setTextAlign(Align.CENTER); mDescent = (int) mPaint.descent(); + // 80 pixels for a 160dpi device would mean half an inch + mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50); mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent me) { if (mSuggestions.size() > 0) { - if (me.getX() + mScrollX < mWordWidth[0] && mScrollX < 10) { + if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) { longPressFirstWord(); } } @@ -150,14 +156,16 @@ public class CandidateView extends View { float distanceX, float distanceY) { final int width = getWidth(); mScrolled = true; - mScrollX += (int) distanceX; - if (mScrollX < 0) { - mScrollX = 0; + int scrollX = getScrollX(); + scrollX += (int) distanceX; + if (scrollX < 0) { + scrollX = 0; } - if (distanceX > 0 && mScrollX + width > mTotalWidth) { - mScrollX -= (int) distanceX; + if (distanceX > 0 && scrollX + width > mTotalWidth) { + scrollX -= (int) distanceX; } - mTargetScrollX = mScrollX; + mTargetScrollX = scrollX; + scrollTo(scrollX, getScrollY()); hidePreview(); invalidate(); return true; @@ -167,7 +175,7 @@ public class CandidateView extends View { setWillNotDraw(false); setHorizontalScrollBarEnabled(false); setVerticalScrollBarEnabled(false); - mScrollX = 0; + scrollTo(0, getScrollY()); } /** @@ -201,7 +209,7 @@ public class CandidateView extends View { if (getBackground() != null) { getBackground().getPadding(mBgPadding); } - mDivider.setBounds(0, mBgPadding.top, mDivider.getIntrinsicWidth(), + mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight()); } int x = 0; @@ -210,7 +218,7 @@ public class CandidateView extends View { final Rect bgPadding = mBgPadding; final Paint paint = mPaint; final int touchX = mTouchX; - final int scrollX = mScrollX; + final int scrollX = getScrollX(); final boolean scrolled = mScrolled; final boolean typedWordValid = mTypedWordValid; final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2; @@ -231,7 +239,7 @@ public class CandidateView extends View { wordWidth = mWordWidth[i]; } else { float textWidth = paint.measureText(suggestion, 0, suggestion.length()); - wordWidth = (int) textWidth + X_GAP * 2; + wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2); mWordWidth[i] = wordWidth; } @@ -251,7 +259,7 @@ public class CandidateView extends View { } if (canvas != null) { - canvas.drawText(suggestion, 0, suggestion.length(), x + X_GAP, y, paint); + canvas.drawText(suggestion, 0, suggestion.length(), x + wordWidth / 2, y, paint); paint.setColor(mColorOther); canvas.translate(x + wordWidth, 0); mDivider.draw(canvas); @@ -261,23 +269,30 @@ public class CandidateView extends View { x += wordWidth; } mTotalWidth = x; - if (mTargetScrollX != mScrollX) { + if (mTargetScrollX != scrollX) { scrollToTarget(); } } private void scrollToTarget() { - if (mTargetScrollX > mScrollX) { - mScrollX += SCROLL_PIXELS; - if (mScrollX >= mTargetScrollX) { - mScrollX = mTargetScrollX; + int scrollX = getScrollX(); + if (mTargetScrollX > scrollX) { + scrollX += SCROLL_PIXELS; + if (scrollX >= mTargetScrollX) { + scrollX = mTargetScrollX; + scrollTo(scrollX, getScrollY()); requestLayout(); + } else { + scrollTo(scrollX, getScrollY()); } } else { - mScrollX -= SCROLL_PIXELS; - if (mScrollX <= mTargetScrollX) { - mScrollX = mTargetScrollX; + scrollX -= SCROLL_PIXELS; + if (scrollX <= mTargetScrollX) { + scrollX = mTargetScrollX; + scrollTo(scrollX, getScrollY()); requestLayout(); + } else { + scrollTo(scrollX, getScrollY()); } } invalidate(); @@ -291,7 +306,7 @@ public class CandidateView extends View { } mShowingCompletions = completions; mTypedWordValid = typedWordValid; - mScrollX = 0; + scrollTo(0, getScrollY()); mTargetScrollX = 0; mHaveMinimalSuggestion = haveMinimalSuggestion; // Compute the total width @@ -305,8 +320,8 @@ public class CandidateView extends View { final int count = mSuggestions.size(); int firstItem = 0; // Actually just before the first item, if at the boundary while (i < count) { - if (mWordX[i] < mScrollX - && mWordX[i] + mWordWidth[i] >= mScrollX - 1) { + if (mWordX[i] < getScrollX() + && mWordX[i] + mWordWidth[i] >= getScrollX() - 1) { firstItem = i; break; } @@ -319,9 +334,10 @@ public class CandidateView extends View { public void scrollNext() { int i = 0; - int targetX = mScrollX; + int scrollX = getScrollX(); + int targetX = scrollX; final int count = mSuggestions.size(); - int rightEdge = mScrollX + getWidth(); + int rightEdge = scrollX + getWidth(); while (i < count) { if (mWordX[i] <= rightEdge && mWordX[i] + mWordWidth[i] >= rightEdge) { @@ -334,7 +350,7 @@ public class CandidateView extends View { } private void updateScrollPosition(int targetX) { - if (targetX != mScrollX) { + if (targetX != getScrollX()) { // TODO: Animate mTargetScrollX = targetX; requestLayout(); @@ -452,7 +468,8 @@ public class CandidateView extends View { + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight(); final int popupHeight = mPreviewText.getMeasuredHeight(); //mPreviewText.setVisibility(INVISIBLE); - mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - mScrollX; + mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX() + + (mWordWidth[wordIndex] - wordWidth) / 2; mPopupPreviewY = - popupHeight; mHandler.removeMessages(MSG_REMOVE_PREVIEW); int [] offsetInWindow = new int[2]; @@ -478,6 +495,7 @@ public class CandidateView extends View { private void longPressFirstWord() { CharSequence word = mSuggestions.get(0); + if (word.length() < 2) return; if (mService.addWordToDictionary(word.toString())) { showPreview(0, getContext().getResources().getString(R.string.added_word, word)); } diff --git a/src/com/android/inputmethod/latin/Dictionary.java b/src/com/android/inputmethod/latin/Dictionary.java index fdf34264a..6c1c856e7 100644 --- a/src/com/android/inputmethod/latin/Dictionary.java +++ b/src/com/android/inputmethod/latin/Dictionary.java @@ -86,4 +86,9 @@ abstract public class Dictionary { return true; } + /** + * Override to clean up any resources. + */ + public void close() { + } } diff --git a/src/com/android/inputmethod/latin/Hints.java b/src/com/android/inputmethod/latin/Hints.java new file mode 100644 index 000000000..689c8d852 --- /dev/null +++ b/src/com/android/inputmethod/latin/Hints.java @@ -0,0 +1,188 @@ +/* + * 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.latin; + +import com.android.inputmethod.voice.SettingsUtil; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.inputmethod.InputConnection; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +/** + * Logic to determine when to display hints on usage to the user. + */ +public class Hints { + public interface Display { + public void showHint(int viewResource); + } + + private static final String TAG = "Hints"; + private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN = + "voice_hint_num_unique_days_shown"; + private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN = + "voice_hint_last_time_shown"; + private static final String PREF_VOICE_INPUT_LAST_TIME_USED = + "voice_input_last_time_used"; + private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT = + "voice_punctuation_hint_view_count"; + private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7; + private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7; + + private Context mContext; + private Display mDisplay; + private boolean mVoiceResultContainedPunctuation; + private int mSwipeHintMaxDaysToShow; + private int mPunctuationHintMaxDisplays; + + // Only show punctuation hint if voice result did not contain punctuation. + static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION + = new HashMap<CharSequence, String>(); + static { + SPEAKABLE_PUNCTUATION.put(",", "comma"); + SPEAKABLE_PUNCTUATION.put(".", "period"); + SPEAKABLE_PUNCTUATION.put("?", "question mark"); + } + + public Hints(Context context, Display display) { + mContext = context; + mDisplay = display; + + ContentResolver cr = mContext.getContentResolver(); + mSwipeHintMaxDaysToShow = SettingsUtil.getSettingsInt( + cr, + SettingsUtil.LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS, + DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW); + mPunctuationHintMaxDisplays = SettingsUtil.getSettingsInt( + cr, + SettingsUtil.LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS, + DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS); + } + + public boolean showSwipeHintIfNecessary(boolean fieldRecommended) { + if (fieldRecommended && shouldShowSwipeHint()) { + showHint(R.layout.voice_swipe_hint); + return true; + } + + return false; + } + + public boolean showPunctuationHintIfNecessary(InputConnection ic) { + if (!mVoiceResultContainedPunctuation + && ic != null + && getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT) + < mPunctuationHintMaxDisplays) { + CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0); + if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) { + showHint(R.layout.voice_punctuation_hint); + return true; + } + } + + return false; + } + + public void registerVoiceResult(String text) { + // Update the current time as the last time voice input was used. + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(mContext).edit(); + editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis()); + editor.commit(); + + mVoiceResultContainedPunctuation = false; + for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) { + if (text.indexOf(s.toString()) >= 0) { + mVoiceResultContainedPunctuation = true; + break; + } + } + } + + private boolean shouldShowSwipeHint() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + + int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); + + // If we've already shown the hint for enough days, we'll return false. + if (numUniqueDaysShown < mSwipeHintMaxDaysToShow) { + + long lastTimeVoiceWasUsed = sp.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0); + + // If the user has used voice today, we'll return false. (We don't show the hint on + // any day that the user has already used voice.) + if (!isFromToday(lastTimeVoiceWasUsed)) { + return true; + } + } + + return false; + } + + /** + * Determines whether the provided time is from some time today (i.e., this day, month, + * and year). + */ + private boolean isFromToday(long timeInMillis) { + if (timeInMillis == 0) return false; + + Calendar today = Calendar.getInstance(); + today.setTimeInMillis(System.currentTimeMillis()); + + Calendar timestamp = Calendar.getInstance(); + timestamp.setTimeInMillis(timeInMillis); + + return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) && + today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) && + today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH)); + } + + private void showHint(int hintViewResource) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + + int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); + long lastTimeHintWasShown = sp.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0); + + // If this is the first time the hint is being shown today, increase the saved values + // to represent that. We don't need to increase the last time the hint was shown unless + // it is a different day from the current value. + if (!isFromToday(lastTimeHintWasShown)) { + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1); + editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis()); + editor.commit(); + } + + if (mDisplay != null) { + mDisplay.showHint(hintViewResource); + } + } + + private int getAndIncrementPref(String pref) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + int value = sp.getInt(pref, 0); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(pref, value + 1); + editor.commit(); + return value; + } +} diff --git a/src/com/android/inputmethod/latin/InputLanguageSelection.java b/src/com/android/inputmethod/latin/InputLanguageSelection.java new file mode 100644 index 000000000..47ace7a6f --- /dev/null +++ b/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -0,0 +1,178 @@ +/* + * 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.latin; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceManager; +import android.util.Log; + +public class InputLanguageSelection extends PreferenceActivity { + + private String mSelectedLanguages; + private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>(); + + private static class Loc implements Comparable { + static Collator sCollator = Collator.getInstance(); + + String label; + Locale locale; + + public Loc(String label, Locale locale) { + this.label = label; + this.locale = locale; + } + + @Override + public String toString() { + return this.label; + } + + public int compareTo(Object o) { + return sCollator.compare(this.label, ((Loc) o).label); + } + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.language_prefs); + // Get the settings preferences + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + mSelectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + String[] languageList = mSelectedLanguages.split(","); + mAvailableLanguages = getUniqueLocales(); + PreferenceGroup parent = getPreferenceScreen(); + for (int i = 0; i < mAvailableLanguages.size(); i++) { + CheckBoxPreference pref = new CheckBoxPreference(this); + Locale locale = mAvailableLanguages.get(i).locale; + pref.setTitle(toTitleCase(locale.getDisplayName(locale))); + boolean checked = isLocaleIn(locale, languageList); + pref.setChecked(checked); + parent.addPreference(pref); + } + } + + private boolean isLocaleIn(Locale locale, String[] list) { + String lang = get5Code(locale); + for (int i = 0; i < list.length; i++) { + if (lang.equalsIgnoreCase(list[i])) return true; + } + // If it matches the current locale + Locale displayLocale = getResources().getConfiguration().locale; + if (lang.equalsIgnoreCase(get5Code(displayLocale))) { + return true; + } + return false; + } + + private String get5Code(Locale locale) { + return locale.getLanguage() + "_" + locale.getCountry(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + // Save the selected languages + String checkedLanguages = ""; + PreferenceGroup parent = getPreferenceScreen(); + int count = parent.getPreferenceCount(); + for (int i = 0; i < count; i++) { + CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i); + if (pref.isChecked()) { + Locale locale = mAvailableLanguages.get(i).locale; + checkedLanguages += get5Code(locale) + ","; + } + } + if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + Editor editor = sp.edit(); + editor.putString(LatinIME.PREF_SELECTED_LANGUAGES, checkedLanguages); + editor.commit(); + } + + ArrayList<Loc> getUniqueLocales() { + String[] locales = getAssets().getLocales(); + Arrays.sort(locales); + ArrayList<Loc> uniqueLocales = new ArrayList<Loc>(); + + final int origSize = locales.length; + Loc[] preprocess = new Loc[origSize]; + int finalSize = 0; + for (int i = 0 ; i < origSize; i++ ) { + String s = locales[i]; + int len = s.length(); + if (len == 5) { + String language = s.substring(0, 2); + String country = s.substring(3, 5); + Locale l = new Locale(language, country); + + if (finalSize == 0) { + preprocess[finalSize++] = + new Loc(toTitleCase(l.getDisplayName(l)), l); + } else { + // check previous entry: + // same lang and a country -> upgrade to full name and + // insert ours with full name + // diff lang -> insert ours with lang-only name + if (preprocess[finalSize-1].locale.getLanguage().equals( + language)) { + preprocess[finalSize-1].label = toTitleCase( + preprocess[finalSize-1].locale.getDisplayName()); + preprocess[finalSize++] = + new Loc(toTitleCase(l.getDisplayName()), l); + } else { + String displayName; + if (s.equals("zz_ZZ")) { + } else { + displayName = toTitleCase(l.getDisplayName(l)); + preprocess[finalSize++] = new Loc(displayName, l); + } + } + } + } + } + for (int i = 0; i < finalSize ; i++) { + uniqueLocales.add(preprocess[i]); + } + return uniqueLocales; + } + + private static String toTitleCase(String s) { + if (s.length() == 0) { + return s; + } + + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + +} diff --git a/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/src/com/android/inputmethod/latin/KeyboardSwitcher.java index c82587b71..529edeb81 100644 --- a/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ b/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -17,8 +17,14 @@ package com.android.inputmethod.latin; import java.util.HashMap; +import java.util.Locale; import java.util.Map; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.inputmethodservice.InputMethodService; + public class KeyboardSwitcher { public static final int MODE_TEXT = 1; @@ -27,6 +33,7 @@ public class KeyboardSwitcher { public static final int MODE_URL = 4; public static final int MODE_EMAIL = 5; public static final int MODE_IM = 6; + public static final int MODE_WEB = 7; public static final int MODE_TEXT_QWERTY = 0; public static final int MODE_TEXT_ALPHA = 1; @@ -36,34 +43,62 @@ public class KeyboardSwitcher { public static final int KEYBOARDMODE_URL = R.id.mode_url; public static final int KEYBOARDMODE_EMAIL = R.id.mode_email; public static final int KEYBOARDMODE_IM = R.id.mode_im; + public static final int KEYBOARDMODE_WEB = R.id.mode_webentry; private static final int SYMBOLS_MODE_STATE_NONE = 0; private static final int SYMBOLS_MODE_STATE_BEGIN = 1; private static final int SYMBOLS_MODE_STATE_SYMBOL = 2; LatinKeyboardView mInputView; - LatinIME mContext; + private static final int[] ALPHABET_MODES = { + KEYBOARDMODE_NORMAL, + KEYBOARDMODE_URL, + KEYBOARDMODE_EMAIL, + KEYBOARDMODE_IM, + KEYBOARDMODE_WEB}; + + //LatinIME mContext; + Context mContext; + InputMethodService mInputMethodService; private KeyboardId mSymbolsId; private KeyboardId mSymbolsShiftedId; private KeyboardId mCurrentId; private Map<KeyboardId, LatinKeyboard> mKeyboards; - - private int mMode; + + private int mMode; /** One of the MODE_XXX values */ private int mImeOptions; private int mTextMode = MODE_TEXT_QWERTY; private boolean mIsSymbols; + private boolean mHasVoice; + private boolean mVoiceOnPrimary; private boolean mPreferSymbols; private int mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; private int mLastDisplayWidth; + private LanguageSwitcher mLanguageSwitcher; + private Locale mInputLocale; + private boolean mEnableMultipleLanguages; - KeyboardSwitcher(LatinIME context) { + KeyboardSwitcher(Context context, InputMethodService ims) { mContext = context; mKeyboards = new HashMap<KeyboardId, LatinKeyboard>(); - mSymbolsId = new KeyboardId(R.xml.kbd_symbols); - mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift); + mSymbolsId = new KeyboardId(R.xml.kbd_symbols, false); + mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, false); + mInputMethodService = ims; + } + + /** + * Sets the input locale, when there are multiple locales for input. + * If no locale switching is required, then the locale should be set to null. + * @param locale the current input locale, or null for default locale with no locale + * button. + */ + void setLanguageSwitcher(LanguageSwitcher languageSwitcher) { + mLanguageSwitcher = languageSwitcher; + mInputLocale = mLanguageSwitcher.getInputLocale(); + mEnableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 1; } void setInputView(LatinKeyboardView inputView) { @@ -75,12 +110,13 @@ public class KeyboardSwitcher { // Configuration change is coming after the keyboard gets recreated. So don't rely on that. // If keyboards have already been made, check if we have a screen width change and // create the keyboard layouts again at the correct orientation - int displayWidth = mContext.getMaxWidth(); + int displayWidth = mInputMethodService.getMaxWidth(); if (displayWidth == mLastDisplayWidth) return; mLastDisplayWidth = displayWidth; if (!forceCreate) mKeyboards.clear(); - mSymbolsId = new KeyboardId(R.xml.kbd_symbols); - mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift); + mSymbolsId = new KeyboardId(R.xml.kbd_symbols, mHasVoice && !mVoiceOnPrimary); + mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, + mHasVoice && !mVoiceOnPrimary); } /** @@ -89,17 +125,19 @@ public class KeyboardSwitcher { */ private static class KeyboardId { public int mXml; - public int mMode; + public int mKeyboardMode; /** A KEYBOARDMODE_XXX value */ public boolean mEnableShiftLock; + public boolean mHasVoice; - public KeyboardId(int xml, int mode, boolean enableShiftLock) { + public KeyboardId(int xml, int mode, boolean enableShiftLock, boolean hasVoice) { this.mXml = xml; - this.mMode = mode; + this.mKeyboardMode = mode; this.mEnableShiftLock = enableShiftLock; + this.mHasVoice = hasVoice; } - public KeyboardId(int xml) { - this(xml, 0, false); + public KeyboardId(int xml, boolean hasVoice) { + this(xml, 0, false, hasVoice); } public boolean equals(Object other) { @@ -107,27 +145,49 @@ public class KeyboardSwitcher { } public boolean equals(KeyboardId other) { - return other.mXml == this.mXml && other.mMode == this.mMode; + return other.mXml == this.mXml + && other.mKeyboardMode == this.mKeyboardMode + && other.mEnableShiftLock == this.mEnableShiftLock + && other.mHasVoice == this.mHasVoice; } public int hashCode() { - return (mXml + 1) * (mMode + 1) * (mEnableShiftLock ? 2 : 1); + return (mXml + 1) * (mKeyboardMode + 1) * (mEnableShiftLock ? 2 : 1) + * (mHasVoice ? 4 : 8); + } + } + + void setVoiceMode(boolean enableVoice, boolean voiceOnPrimary) { + if (enableVoice != mHasVoice || voiceOnPrimary != mVoiceOnPrimary) { + mKeyboards.clear(); } + mHasVoice = enableVoice; + mVoiceOnPrimary = voiceOnPrimary; + setKeyboardMode(mMode, mImeOptions, mHasVoice, + mIsSymbols); } - void setKeyboardMode(int mode, int imeOptions) { + boolean hasVoiceButton(boolean isSymbols) { + return mHasVoice && (isSymbols != mVoiceOnPrimary); + } + + void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) { mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; mPreferSymbols = mode == MODE_SYMBOLS; - setKeyboardMode(mode == MODE_SYMBOLS ? MODE_TEXT : mode, imeOptions, + setKeyboardMode(mode == MODE_SYMBOLS ? MODE_TEXT : mode, imeOptions, enableVoice, mPreferSymbols); } - void setKeyboardMode(int mode, int imeOptions, boolean isSymbols) { + void setKeyboardMode(int mode, int imeOptions, + boolean enableVoice, boolean isSymbols) { mMode = mode; mImeOptions = imeOptions; + mHasVoice = enableVoice; mIsSymbols = isSymbols; + mInputView.setPreviewEnabled(true); KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols); + LatinKeyboard keyboard = getKeyboard(id); if (mode == MODE_PHONE) { @@ -140,45 +200,65 @@ public class KeyboardSwitcher { keyboard.setShifted(false); keyboard.setShiftLocked(keyboard.isShiftLocked()); keyboard.setImeOptions(mContext.getResources(), mMode, imeOptions); - } private LatinKeyboard getKeyboard(KeyboardId id) { if (!mKeyboards.containsKey(id)) { + Resources orig = mContext.getResources(); + Configuration conf = orig.getConfiguration(); + Locale saveLocale = conf.locale; + conf.locale = mInputLocale; + orig.updateConfiguration(conf, null); LatinKeyboard keyboard = new LatinKeyboard( - mContext, id.mXml, id.mMode); + mContext, id.mXml, id.mKeyboardMode, id.mHasVoice); + keyboard.setLanguageSwitcher(mLanguageSwitcher); + if (id.mKeyboardMode == KEYBOARDMODE_NORMAL + || id.mKeyboardMode == KEYBOARDMODE_URL + || id.mKeyboardMode == KEYBOARDMODE_IM + || id.mKeyboardMode == KEYBOARDMODE_EMAIL + || id.mKeyboardMode == KEYBOARDMODE_WEB + ) { + keyboard.setExtension(R.xml.kbd_extension); + } + if (id.mEnableShiftLock) { keyboard.enableShiftLock(); } mKeyboards.put(id, keyboard); + + conf.locale = saveLocale; + orig.updateConfiguration(conf, null); } return mKeyboards.get(id); } private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { + boolean hasVoice = hasVoiceButton(isSymbols); if (isSymbols) { return (mode == MODE_PHONE) - ? new KeyboardId(R.xml.kbd_phone_symbols) : new KeyboardId(R.xml.kbd_symbols); + ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice) + : new KeyboardId(R.xml.kbd_symbols, hasVoice); } - switch (mode) { case MODE_TEXT: if (mTextMode == MODE_TEXT_QWERTY) { - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_NORMAL, true); + return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_NORMAL, true, hasVoice); } else if (mTextMode == MODE_TEXT_ALPHA) { - return new KeyboardId(R.xml.kbd_alpha, KEYBOARDMODE_NORMAL, true); + return new KeyboardId(R.xml.kbd_alpha, KEYBOARDMODE_NORMAL, true, hasVoice); } break; case MODE_SYMBOLS: - return new KeyboardId(R.xml.kbd_symbols); + return new KeyboardId(R.xml.kbd_symbols, hasVoice); case MODE_PHONE: - return new KeyboardId(R.xml.kbd_phone); + return new KeyboardId(R.xml.kbd_phone, hasVoice); case MODE_URL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true); + return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true, hasVoice); case MODE_EMAIL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL, true); + return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL, true, hasVoice); case MODE_IM: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_IM, true); + return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_IM, true, hasVoice); + case MODE_WEB: + return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_WEB, true, hasVoice); } return null; } @@ -200,7 +280,7 @@ public class KeyboardSwitcher { mTextMode = position; } if (isTextMode()) { - setKeyboardMode(MODE_TEXT, mImeOptions); + setKeyboardMode(MODE_TEXT, mImeOptions, mHasVoice); } } @@ -209,11 +289,13 @@ public class KeyboardSwitcher { } boolean isAlphabetMode() { - KeyboardId current = mCurrentId; - return current.mMode == KEYBOARDMODE_NORMAL - || current.mMode == KEYBOARDMODE_URL - || current.mMode == KEYBOARDMODE_EMAIL - || current.mMode == KEYBOARDMODE_IM; + int currentMode = mCurrentId.mKeyboardMode; + for (Integer mode : ALPHABET_MODES) { + if (currentMode == mode) { + return true; + } + } + return false; } void toggleShift() { @@ -237,7 +319,7 @@ public class KeyboardSwitcher { } void toggleSymbols() { - setKeyboardMode(mMode, mImeOptions, !mIsSymbols); + setKeyboardMode(mMode, mImeOptions, mHasVoice, !mIsSymbols); if (mIsSymbols && !mPreferSymbols) { mSymbolsModeState = SYMBOLS_MODE_STATE_BEGIN; } else { diff --git a/src/com/android/inputmethod/latin/LanguageSwitcher.java b/src/com/android/inputmethod/latin/LanguageSwitcher.java new file mode 100644 index 000000000..3fa882e44 --- /dev/null +++ b/src/com/android/inputmethod/latin/LanguageSwitcher.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2010 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.latin; + +import java.util.Locale; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +/** + * Keeps track of list of selected input languages and the current + * input language that the user has selected. + */ +public class LanguageSwitcher { + + private Locale[] mLocales; + private LatinIME mIme; + private String[] mSelectedLanguageArray; + private String mSelectedLanguages; + private int mCurrentIndex = 0; + private String mDefaultInputLanguage; + private Locale mDefaultInputLocale; + + public LanguageSwitcher(LatinIME ime) { + mIme = ime; + mLocales = new Locale[0]; + } + + public Locale[] getLocales() { + return mLocales; + } + + public int getLocaleCount() { + return mLocales.length; + } + + /** + * Loads the currently selected input languages from shared preferences. + * @param sp + * @return whether there was any change + */ + public boolean loadLocales(SharedPreferences sp) { + String selectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, null); + String currentLanguage = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null); + if (selectedLanguages == null || selectedLanguages.length() < 1) { + loadDefaults(); + if (mLocales.length == 0) { + return false; + } + mLocales = new Locale[0]; + return true; + } + if (selectedLanguages.equals(mSelectedLanguages)) { + return false; + } + mSelectedLanguageArray = selectedLanguages.split(","); + mSelectedLanguages = selectedLanguages; // Cache it for comparison later + constructLocales(); + mCurrentIndex = 0; + if (currentLanguage != null) { + // Find the index + mCurrentIndex = 0; + for (int i = 0; i < mLocales.length; i++) { + if (mSelectedLanguageArray[i].equals(currentLanguage)) { + mCurrentIndex = i; + break; + } + } + // If we didn't find the index, use the first one + } + return true; + } + + private void loadDefaults() { + mDefaultInputLocale = mIme.getResources().getConfiguration().locale; + mDefaultInputLanguage = mDefaultInputLocale.getLanguage() + "_" + + mDefaultInputLocale.getCountry(); + } + + private void constructLocales() { + mLocales = new Locale[mSelectedLanguageArray.length]; + for (int i = 0; i < mLocales.length; i++) { + mLocales[i] = new Locale(mSelectedLanguageArray[i]); + } + } + + /** + * Returns the currently selected input language code, or the display language code if + * no specific locale was selected for input. + */ + public String getInputLanguage() { + if (getLocaleCount() == 0) return mDefaultInputLanguage; + + return mSelectedLanguageArray[mCurrentIndex]; + } + + /** + * Returns the list of enabled language codes. + */ + public String[] getEnabledLanguages() { + return mSelectedLanguageArray; + } + + /** + * Returns the currently selected input locale, or the display locale if no specific + * locale was selected for input. + * @return + */ + public Locale getInputLocale() { + if (getLocaleCount() == 0) return mDefaultInputLocale; + + return mLocales[mCurrentIndex]; + } + + /** + * Returns the next input locale in the list. Wraps around to the beginning of the + * list if we're at the end of the list. + * @return + */ + public Locale getNextInputLocale() { + if (getLocaleCount() == 0) return mDefaultInputLocale; + + return mLocales[(mCurrentIndex + 1) % mLocales.length]; + } + + /** + * Returns the previous input locale in the list. Wraps around to the end of the + * list if we're at the beginning of the list. + * @return + */ + public Locale getPrevInputLocale() { + if (getLocaleCount() == 0) return mDefaultInputLocale; + + return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length]; + } + + public void reset() { + mCurrentIndex = 0; + } + + public void next() { + mCurrentIndex++; + if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around + } + + public void prev() { + mCurrentIndex--; + if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around + } + + public void persist() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme); + Editor editor = sp.edit(); + editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage()); + editor.commit(); + } +} diff --git a/src/com/android/inputmethod/latin/LatinIME.java b/src/com/android/inputmethod/latin/LatinIME.java index a6cf312d2..8b9c0cac7 100644 --- a/src/com/android/inputmethod/latin/LatinIME.java +++ b/src/com/android/inputmethod/latin/LatinIME.java @@ -1,12 +1,12 @@ /* * 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 @@ -16,14 +16,20 @@ package com.android.inputmethod.latin; +import com.google.android.collect.Lists; + import android.app.AlertDialog; +import android.backup.BackupManager; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.ContextWrapper; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.content.res.Configuration; +import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.Keyboard; import android.inputmethodservice.KeyboardView; @@ -41,38 +47,98 @@ import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import com.android.inputmethod.voice.EditingUtil; +import com.android.inputmethod.voice.FieldContext; +import com.android.inputmethod.voice.SettingsUtil; +import com.android.inputmethod.voice.VoiceInput; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Input method implementation for Qwerty'ish keyboard. */ -public class LatinIME extends InputMethodService - implements KeyboardView.OnKeyboardActionListener { +public class LatinIME extends InputMethodService + implements KeyboardView.OnKeyboardActionListener, + VoiceInput.UiListener, + SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "LatinIME"; static final boolean DEBUG = false; static final boolean TRACE = false; - + static final boolean VOICE_INSTALLED = true; + static final boolean ENABLE_VOICE_BUTTON = true; + private static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String PREF_SOUND_ON = "sound_on"; private static final String PREF_AUTO_CAP = "auto_cap"; private static final String PREF_QUICK_FIXES = "quick_fixes"; private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; private static final String PREF_AUTO_COMPLETE = "auto_complete"; + private static final String PREF_ENABLE_VOICE = "enable_voice_input"; + private static final String PREF_VOICE_SERVER_URL = "voice_server_url"; + private static final String PREF_VOICE_MAIN = "voice_on_main"; + + // Whether or not the user has used voice input before (and thus, whether to show the + // first-run warning dialog or not). + private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input"; + + // Whether or not the user has used voice input from an unsupported locale UI before. + // For example, the user has a Chinese UI but activates voice input. + private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE = + "has_used_voice_input_unsupported_locale"; + + // A list of locales which are supported by default for voice input, unless we get a + // different list from Gservices. + public static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = + "en " + + "en_US " + + "en_GB " + + "en_AU " + + "en_CA " + + "en_IE " + + "en_IN " + + "en_NZ " + + "en_SG " + + "en_ZA "; + + // The private IME option used to indicate that no microphone should be shown for a + // given text field. For instance this is specified by the search dialog when the + // dialog is already showing a voice search button. + private static final String IME_OPTION_NO_MICROPHONE = "nm"; + + public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; + public static final String PREF_INPUT_LANGUAGE = "input_language"; private static final int MSG_UPDATE_SUGGESTIONS = 0; private static final int MSG_START_TUTORIAL = 1; private static final int MSG_UPDATE_SHIFT_STATE = 2; - + private static final int MSG_VOICE_RESULTS = 3; + private static final int MSG_START_LISTENING_AFTER_SWIPE = 4; + + // If we detect a swipe gesture within N ms of typing, then swipe is + // ignored, since it may in fact be two key presses in quick succession. + private static final long MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE = 1000; + + // If we detect a swipe gesture, and the user types N ms later, cancel the + // swipe since it was probably a false trigger. + private static final long MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING = 500; + // How many continuous deletes at which to start deleting at a higher speed. private static final int DELETE_ACCELERATE_AT = 20; // Key events coming any faster than this are long-presses. @@ -84,54 +150,79 @@ public class LatinIME extends InputMethodService // A word that is frequently typed and get's promoted to the user dictionary, uses this // frequency. static final int FREQUENCY_FOR_AUTO_ADD = 250; - + static final int KEYCODE_ENTER = '\n'; static final int KEYCODE_SPACE = ' '; // Contextual menu positions private static final int POS_SETTINGS = 0; private static final int POS_METHOD = 1; - + private LatinKeyboardView mInputView; private CandidateViewContainer mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; private CompletionInfo[] mCompletions; - + private AlertDialog mOptionsDialog; - + private AlertDialog mVoiceWarningDialog; + KeyboardSwitcher mKeyboardSwitcher; - + private UserDictionary mUserDictionary; private ContactsDictionary mContactsDictionary; private ExpandableDictionary mAutoDictionary; - + + private Hints mHints; + + Resources mResources; + private String mLocale; + private LanguageSwitcher mLanguageSwitcher; private StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); private int mCommittedLength; private boolean mPredicting; + private boolean mRecognizing; + private boolean mAfterVoiceInput; + private boolean mImmediatelyAfterVoiceInput; + private boolean mShowingVoiceSuggestions; + private boolean mImmediatelyAfterVoiceSuggestions; + private boolean mVoiceInputHighlighted; + private boolean mEnableVoiceButton; private CharSequence mBestWord; private boolean mPredictionOn; private boolean mCompletionOn; + private boolean mHasDictionary; private boolean mAutoSpace; + private boolean mAutoCorrectEnabled; private boolean mAutoCorrectOn; private boolean mCapsLock; + private boolean mPasswordText; + private boolean mEmailText; private boolean mVibrateOn; private boolean mSoundOn; private boolean mAutoCap; private boolean mQuickFixes; + private boolean mHasUsedVoiceInput; + private boolean mHasUsedVoiceInputUnsupportedLocale; + private boolean mLocaleSupportedForVoiceInput; private boolean mShowSuggestions; + private boolean mSuggestionShouldReplaceCurrentWord; + private boolean mIsShowingHint; private int mCorrectionMode; + private boolean mEnableVoice = true; + private boolean mVoiceOnPrimary; private int mOrientation; + private List<CharSequence> mSuggestPuncList; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; private CharSequence mJustRevertedSeparator; private int mDeleteCount; private long mLastKeyTime; - + private Tutorial mTutorial; private Vibrator mVibrator; @@ -144,7 +235,19 @@ public class LatinIME extends InputMethodService private String mWordSeparators; private String mSentenceSeparators; - + private VoiceInput mVoiceInput; + private VoiceResults mVoiceResults = new VoiceResults(); + private long mSwipeTriggerTimeMillis; + + // For each word, a list of potential replacements, usually from voice. + private Map<String, List<CharSequence>> mWordToSuggestions = new HashMap(); + + private class VoiceResults { + List<String> candidates; + Map<String, List<CharSequence>> alternatives; + } + private boolean mRefreshKeyboardRequired; + Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -166,6 +269,13 @@ public class LatinIME extends InputMethodService case MSG_UPDATE_SHIFT_STATE: updateShiftKeyState(getCurrentInputEditorInfo()); break; + case MSG_VOICE_RESULTS: + handleVoiceResults(); + break; + case MSG_START_LISTENING_AFTER_SWIPE: + if (mLastKeyTime < mSwipeTriggerTimeMillis) { + startListening(true); + } } } }; @@ -173,36 +283,83 @@ public class LatinIME extends InputMethodService @Override public void onCreate() { super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); - mKeyboardSwitcher = new KeyboardSwitcher(this); - final Configuration conf = getResources().getConfiguration(); - initSuggest(conf.locale.toString()); + mResources = getResources(); + final Configuration conf = mResources.getConfiguration(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mLanguageSwitcher = new LanguageSwitcher(this); + mLanguageSwitcher.loadLocales(prefs); + mKeyboardSwitcher = new KeyboardSwitcher(this, this); + mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); + boolean enableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 0; + String inputLanguage = mLanguageSwitcher.getInputLanguage(); + if (inputLanguage == null) { + inputLanguage = conf.locale.toString(); + } + initSuggest(inputLanguage); mOrientation = conf.orientation; + initSuggestPuncList(); - mVibrateDuration = getResources().getInteger(R.integer.vibrate_duration_ms); + mVibrateDuration = mResources.getInteger(R.integer.vibrate_duration_ms); // register to receive ringer mode changes for silent mode IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mReceiver, filter); + if (VOICE_INSTALLED) { + mVoiceInput = new VoiceInput(this, this); + mHints = new Hints(this, new Hints.Display() { + public void showHint(int viewResource) { + LayoutInflater inflater = (LayoutInflater) getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(viewResource, null); + setCandidatesView(view); + setCandidatesViewShown(true); + mIsShowingHint = true; + } + }); + } + prefs.registerOnSharedPreferenceChangeListener(this); } - + private void initSuggest(String locale) { mLocale = locale; + + Resources orig = getResources(); + Configuration conf = orig.getConfiguration(); + Locale saveLocale = conf.locale; + conf.locale = new Locale(locale); + orig.updateConfiguration(conf, orig.getDisplayMetrics()); + if (mSuggest != null) { + mSuggest.close(); + } mSuggest = new Suggest(this, R.raw.main); - mSuggest.setCorrectionMode(mCorrectionMode); + if (mUserDictionary != null) mUserDictionary.close(); mUserDictionary = new UserDictionary(this); - mContactsDictionary = new ContactsDictionary(this); - mAutoDictionary = new AutoDictionary(this); + if (mContactsDictionary == null) { + mContactsDictionary = new ContactsDictionary(this); + } + // TODO: Save and restore the dictionary for the current input language. + if (mAutoDictionary == null) { + mAutoDictionary = new AutoDictionary(this); + } mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); mSuggest.setAutoDictionary(mAutoDictionary); - mWordSeparators = getResources().getString(R.string.word_separators); - mSentenceSeparators = getResources().getString(R.string.sentence_separators); + updateCorrectionMode(); + mWordSeparators = mResources.getString(R.string.word_separators); + mSentenceSeparators = mResources.getString(R.string.sentence_separators); + + conf.locale = saveLocale; + orig.updateConfiguration(conf, orig.getDisplayMetrics()); } - - @Override public void onDestroy() { + + @Override + public void onDestroy() { mUserDictionary.close(); mContactsDictionary.close(); unregisterReceiver(mReceiver); + if (VOICE_INSTALLED) { + mVoiceInput.destroy(); + } super.onDestroy(); } @@ -213,13 +370,12 @@ public class LatinIME extends InputMethodService } // If orientation changed while predicting, commit the change if (conf.orientation != mOrientation) { - commitTyped(getCurrentInputConnection()); + InputConnection ic = getCurrentInputConnection(); + commitTyped(ic); + if (ic != null) ic.finishComposingText(); // For voice input mOrientation = conf.orientation; } - if (mKeyboardSwitcher == null) { - mKeyboardSwitcher = new KeyboardSwitcher(this); - } - mKeyboardSwitcher.makeKeyboards(true); + reloadKeyboards(); super.onConfigurationChanged(conf); } @@ -230,11 +386,28 @@ public class LatinIME extends InputMethodService mKeyboardSwitcher.setInputView(mInputView); mKeyboardSwitcher.makeKeyboards(true); mInputView.setOnKeyboardActionListener(this); - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, 0); + mKeyboardSwitcher.setKeyboardMode( + KeyboardSwitcher.MODE_TEXT, 0, + shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); return mInputView; } @Override + public void onInitializeInterface() { + // Create a new view associated with voice input if the old + // view is stuck in another layout (e.g. if switching from + // portrait to landscape while speaking) + // NOTE: This must be done here because for some reason + // onCreateInputView isn't called after an orientation change while + // speech rec is in progress. + if (mVoiceInput != null && mVoiceInput.getView().getParent() != null) { + mVoiceInput.newView(); + } + + super.onInitializeInterface(); + } + + @Override public View onCreateCandidatesView() { mKeyboardSwitcher.makeKeyboards(true); mCandidateViewContainer = (CandidateViewContainer) getLayoutInflater().inflate( @@ -246,43 +419,71 @@ public class LatinIME extends InputMethodService return mCandidateViewContainer; } - @Override + @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { // In landscape mode, this method gets called without the input view being created. if (mInputView == null) { return; } + if (mRefreshKeyboardRequired) { + mRefreshKeyboardRequired = false; + toggleLanguage(true, true); + } + mKeyboardSwitcher.makeKeyboards(false); TextEntryState.newSession(this); + // Most such things we decide below in the switch statement, but we need to know + // now whether this is a password text field, because we need to know now (before + // the switch statement) whether we want to enable the voice button. + mPasswordText = false; + int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; + if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || + variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { + mPasswordText = true; + } + + mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute); + final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice; + + mAfterVoiceInput = false; + mImmediatelyAfterVoiceInput = false; + mShowingVoiceSuggestions = false; + mImmediatelyAfterVoiceSuggestions = false; + mVoiceInputHighlighted = false; boolean disableAutoCorrect = false; + mWordToSuggestions.clear(); + mInputTypeNoAutoCorrect = false; mPredictionOn = false; mCompletionOn = false; mCompletions = null; mCapsLock = false; - switch (attribute.inputType&EditorInfo.TYPE_MASK_CLASS) { + mEmailText = false; + switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { case EditorInfo.TYPE_CLASS_NUMBER: case EditorInfo.TYPE_CLASS_DATETIME: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_SYMBOLS, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); break; case EditorInfo.TYPE_CLASS_PHONE: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); break; case EditorInfo.TYPE_CLASS_TEXT: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); //startPrediction(); mPredictionOn = true; // Make sure that passwords are not displayed in candidate view - int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { mPredictionOn = false; } + if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { + mEmailText = true; + } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { mAutoSpace = false; @@ -292,33 +493,35 @@ public class LatinIME extends InputMethodService if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { mPredictionOn = false; mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { mPredictionOn = false; mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { mPredictionOn = false; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB, + attribute.imeOptions, enableVoiceButton); // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { - disableAutoCorrect = true; + mInputTypeNoAutoCorrect = true; } } // If NO_SUGGESTIONS is set, don't do prediction. if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { mPredictionOn = false; - disableAutoCorrect = true; + mInputTypeNoAutoCorrect = true; } // If it's not multiline and the autoCorrect flag is not set, then don't correct if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { - disableAutoCorrect = true; + mInputTypeNoAutoCorrect = true; } if ((attribute.inputType&EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { mPredictionOn = false; @@ -328,16 +531,18 @@ public class LatinIME extends InputMethodService break; default: mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions); + attribute.imeOptions, enableVoiceButton); updateShiftKeyState(attribute); } mInputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; - setCandidatesViewShown(false); - if (mCandidateView != null) mCandidateView.setSuggestions(null, false, false, false); loadSettings(); + + setCandidatesViewShown(false); + setSuggestions(null, false, false, false); + // Override auto correct if (disableAutoCorrect) { mAutoCorrectOn = false; @@ -345,10 +550,12 @@ public class LatinIME extends InputMethodService mCorrectionMode = Suggest.CORRECTION_BASIC; } } + // If the dictionary is not big enough, don't auto correct + mHasDictionary = mSuggest.hasMainDictionary(); + + updateCorrectionMode(); + mInputView.setProximityCorrectionEnabled(true); - if (mSuggest != null) { - mSuggest.setCorrectionMode(mCorrectionMode); - } mPredictionOn = mPredictionOn && mCorrectionMode > 0; checkTutorial(attribute.privateImeOptions); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); @@ -358,9 +565,34 @@ public class LatinIME extends InputMethodService public void onFinishInput() { super.onFinishInput(); + if (VOICE_INSTALLED && mAfterVoiceInput) { + mVoiceInput.logInputEnded(); + } + + if (VOICE_INSTALLED) { + mVoiceInput.flushLogs(); + } + if (mInputView != null) { mInputView.closing(); } + if (VOICE_INSTALLED && mRecognizing) { + mVoiceInput.cancel(); + } + } + + @Override + public void onUpdateExtractedText(int token, ExtractedText text) { + super.onUpdateExtractedText(token, text); + InputConnection ic = getCurrentInputConnection(); + if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) { + mVoiceInput.logTextModified(); + + if (mHints.showPunctuationHintIfNecessary(ic)) { + mVoiceInput.logPunctuationHintDisplayed(); + } + } + mImmediatelyAfterVoiceInput = false; } @Override @@ -369,10 +601,22 @@ public class LatinIME extends InputMethodService int candidatesStart, int candidatesEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); + + if (DEBUG) { + Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + + ", ose=" + oldSelEnd + + ", nss=" + newSelStart + + ", nse=" + newSelEnd + + ", cs=" + candidatesStart + + ", ce=" + candidatesEnd); + } + + mSuggestionShouldReplaceCurrentWord = false; // If the current selection in the text view changes, we should // clear whatever candidate text we have. - if (mComposing.length() > 0 && mPredicting && (newSelStart != candidatesEnd - || newSelEnd != candidatesEnd)) { + if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted) + && (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd))) { mComposing.setLength(0); mPredicting = false; updateSuggestions(); @@ -381,25 +625,58 @@ public class LatinIME extends InputMethodService if (ic != null) { ic.finishComposingText(); } + mVoiceInputHighlighted = false; } else if (!mPredicting && !mJustAccepted && TextEntryState.getState() == TextEntryState.STATE_ACCEPTED_DEFAULT) { TextEntryState.reset(); } mJustAccepted = false; postUpdateShiftKeyState(); + + if (VOICE_INSTALLED) { + if (mShowingVoiceSuggestions) { + if (mImmediatelyAfterVoiceSuggestions) { + mImmediatelyAfterVoiceSuggestions = false; + } else { + updateSuggestions(); + mShowingVoiceSuggestions = false; + } + } + if (VoiceInput.ENABLE_WORD_CORRECTIONS) { + // If we have alternatives for the current word, then show them. + String word = EditingUtil.getWordAtCursor( + getCurrentInputConnection(), getWordSeparators()); + if (word != null && mWordToSuggestions.containsKey(word.trim())) { + mSuggestionShouldReplaceCurrentWord = true; + final List<CharSequence> suggestions = mWordToSuggestions.get(word.trim()); + + setSuggestions(suggestions, false, true, true); + setCandidatesViewShown(true); + } + } + } } @Override public void hideWindow() { + if (mAfterVoiceInput) mVoiceInput.logInputEnded(); if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } + if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { + mVoiceInput.logKeyboardWarningDialogDismissed(); + mVoiceWarningDialog.dismiss(); + mVoiceWarningDialog = null; + } if (mTutorial != null) { mTutorial.close(); mTutorial = null; } + if (VOICE_INSTALLED & mRecognizing) { + mVoiceInput.cancel(); + } super.hideWindow(); TextEntryState.endSession(); } @@ -415,17 +692,17 @@ public class LatinIME extends InputMethodService if (mCompletionOn) { mCompletions = completions; if (completions == null) { - mCandidateView.setSuggestions(null, false, false, false); + setSuggestions(null, false, false, false); return; } - + List<CharSequence> stringList = new ArrayList<CharSequence>(); for (int i=0; i<(completions != null ? completions.length : 0); i++) { CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } //CharSequence typedWord = mWord.getTypedWord(); - mCandidateView.setSuggestions(stringList, true, true, true); + setSuggestions(stringList, true, true, true); mBestWord = null; setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); } @@ -438,7 +715,7 @@ public class LatinIME extends InputMethodService super.setCandidatesViewShown(shown); } } - + @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); @@ -446,7 +723,7 @@ public class LatinIME extends InputMethodService outInsets.contentTopInsets = outInsets.visibleTopInsets; } } - + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { @@ -486,7 +763,7 @@ public class LatinIME extends InputMethodService } // Enable shift key and DPAD to do selections if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { - event = new KeyEvent(event.getDownTime(), event.getEventTime(), + event = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); @@ -499,6 +776,31 @@ public class LatinIME extends InputMethodService return super.onKeyUp(keyCode, event); } + private void revertVoiceInput() { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.commitText("", 1); + updateSuggestions(); + mVoiceInputHighlighted = false; + } + + private void commitVoiceInput() { + InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.finishComposingText(); + updateSuggestions(); + mVoiceInputHighlighted = false; + } + + private void reloadKeyboards() { + if (mKeyboardSwitcher == null) { + mKeyboardSwitcher = new KeyboardSwitcher(this, this); + } + mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); + if (mInputView != null) { + mKeyboardSwitcher.setVoiceMode(mEnableVoice, mVoiceOnPrimary); + } + mKeyboardSwitcher.makeKeyboards(true); + } + private void commitTyped(InputConnection inputConnection) { if (mPredicting) { mPredicting = false; @@ -523,15 +825,19 @@ public class LatinIME extends InputMethodService InputConnection ic = getCurrentInputConnection(); if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode() && ic != null) { - int caps = 0; - EditorInfo ei = getCurrentInputEditorInfo(); - if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { - caps = ic.getCursorCapsMode(attr.inputType); - } - mInputView.setShifted(mCapsLock || caps != 0); + mInputView.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); } } - + + private int getCursorCapsMode(InputConnection ic, EditorInfo attr) { + int caps = 0; + EditorInfo ei = getCurrentInputEditorInfo(); + if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { + caps = ic.getCursorCapsMode(attr.inputType); + } + return caps; + } + private void swapPunctuationAndSpace() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -545,7 +851,7 @@ public class LatinIME extends InputMethodService updateShiftKeyState(getCurrentInputEditorInfo()); } } - + private void doubleSpace() { //if (!mAutoPunctuate) return; if (mCorrectionMode == Suggest.CORRECTION_NONE) return; @@ -562,7 +868,20 @@ public class LatinIME extends InputMethodService updateShiftKeyState(getCurrentInputEditorInfo()); } } - + + private void maybeRemovePreviousPeriod(CharSequence text) { + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + + // When the text's first character is '.', remove the previous period + // if there is one. + CharSequence lastOne = ic.getTextBeforeCursor(1, 0); + if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == '.' + && text.charAt(0) == '.') { + ic.deleteSurroundingText(1, 0); + } + } + public boolean addWordToDictionary(String word) { mUserDictionary.addWord(word, 128); return true; @@ -575,12 +894,12 @@ public class LatinIME extends InputMethodService return false; } } - + // Implementation of KeyboardViewListener public void onKey(int primaryCode, int[] keyCodes) { long when = SystemClock.uptimeMillis(); - if (primaryCode != Keyboard.KEYCODE_DELETE || + if (primaryCode != Keyboard.KEYCODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } @@ -601,6 +920,12 @@ public class LatinIME extends InputMethodService case LatinKeyboardView.KEYCODE_OPTIONS: showOptionsMenu(); break; + case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE: + toggleLanguage(false, true); + break; + case LatinKeyboardView.KEYCODE_PREV_LANGUAGE: + toggleLanguage(false, false); + break; case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS: if (mCapsLock) { handleShift(); @@ -611,6 +936,14 @@ public class LatinIME extends InputMethodService case Keyboard.KEYCODE_MODE_CHANGE: changeKeyboardMode(); break; + case LatinKeyboardView.KEYCODE_VOICE: + if (VOICE_INSTALLED) { + startListening(false /* was a button press, was not a swipe */); + } + break; + case 9 /*Tab*/: + sendKeyChar((char) primaryCode); + break; default: if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); @@ -624,7 +957,7 @@ public class LatinIME extends InputMethodService changeKeyboardMode(); } } - + public void onText(CharSequence text) { InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -632,6 +965,7 @@ public class LatinIME extends InputMethodService if (mPredicting) { commitTyped(ic); } + maybeRemovePreviousPeriod(text); ic.commitText(text, 1); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); @@ -639,6 +973,10 @@ public class LatinIME extends InputMethodService } private void handleBackspace() { + if (VOICE_INSTALLED && mVoiceInputHighlighted) { + revertVoiceInput(); + return; + } boolean deleteChar = false; InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -673,7 +1011,7 @@ public class LatinIME extends InputMethodService } private void handleShift() { - Keyboard currentKeyboard = mInputView.getKeyboard(); + mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); if (mKeyboardSwitcher.isAlphabetMode()) { // Alphabet keyboard checkToggleCapsLock(); @@ -682,8 +1020,11 @@ public class LatinIME extends InputMethodService mKeyboardSwitcher.toggleShift(); } } - + private void handleCharacter(int primaryCode, int[] keyCodes) { + if (VOICE_INSTALLED && mVoiceInputHighlighted) { + commitVoiceInput(); + } if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { if (!mPredicting) { mPredicting = true; @@ -707,6 +1048,11 @@ public class LatinIME extends InputMethodService mWord.add(primaryCode, keyCodes); InputConnection ic = getCurrentInputConnection(); if (ic != null) { + // If it's the first letter, make note of auto-caps state + if (mWord.size() == 1) { + mWord.setAutoCapitalized( + getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0); + } ic.setComposingText(mComposing, 1); } postUpdateSuggestions(); @@ -719,6 +1065,9 @@ public class LatinIME extends InputMethodService } private void handleSeparator(int primaryCode) { + if (VOICE_INSTALLED && mVoiceInputHighlighted) { + commitVoiceInput(); + } boolean pickedDefault = false; // Handle separator InputConnection ic = getCurrentInputConnection(); @@ -727,12 +1076,12 @@ public class LatinIME extends InputMethodService } if (mPredicting) { // In certain languages where single quote is a separator, it's better - // not to auto correct, but accept the typed word. For instance, + // not to auto correct, but accept the typed word. For instance, // in Italian dov' should not be expanded to dove' because the elision // requires the last vowel to be removed. - if (mAutoCorrectOn && primaryCode != '\'' && - (mJustRevertedSeparator == null - || mJustRevertedSeparator.length() == 0 + if (mAutoCorrectOn && primaryCode != '\'' && + (mJustRevertedSeparator == null + || mJustRevertedSeparator.length() == 0 || mJustRevertedSeparator.charAt(0) != primaryCode)) { pickDefaultSuggestion(); pickedDefault = true; @@ -742,10 +1091,10 @@ public class LatinIME extends InputMethodService } sendKeyChar((char)primaryCode); TextEntryState.typedCharacter((char) primaryCode, true); - if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED + if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED && primaryCode != KEYCODE_ENTER) { swapPunctuationAndSpace(); - } else if (isPredictionOn() && primaryCode == ' ') { + } else if (isPredictionOn() && primaryCode == ' ') { //else if (TextEntryState.STATE_SPACE_AFTER_ACCEPTED) { doubleSpace(); } @@ -757,9 +1106,12 @@ public class LatinIME extends InputMethodService ic.endBatchEdit(); } } - + private void handleClose() { commitTyped(getCurrentInputConnection()); + if (VOICE_INSTALLED & mRecognizing) { + mVoiceInput.cancel(); + } requestHideSelf(0); mInputView.closing(); TextEntryState.endSession(); @@ -770,7 +1122,7 @@ public class LatinIME extends InputMethodService toggleCapsLock(); } } - + private void toggleCapsLock() { mCapsLock = !mCapsLock; if (mKeyboardSwitcher.isAlphabetMode()) { @@ -782,25 +1134,219 @@ public class LatinIME extends InputMethodService mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); } - + private boolean isPredictionOn() { boolean predictionOn = mPredictionOn; //if (isFullscreenMode()) predictionOn &= mPredictionLandscape; return predictionOn; } - + private boolean isCandidateStripVisible() { return isPredictionOn() && mShowSuggestions; } + public void onCancelVoice() { + if (mRecognizing) { + switchToKeyboardView(); + } + } + + private void switchToKeyboardView() { + mHandler.post(new Runnable() { + public void run() { + mRecognizing = false; + if (mInputView != null) { + setInputView(mInputView); + } + updateInputViewShown(); + }}); + } + + private void switchToRecognitionStatusView() { + mHandler.post(new Runnable() { + public void run() { + mRecognizing = true; + setInputView(mVoiceInput.getView()); + updateInputViewShown(); + }}); + } + + private void startListening(boolean swipe) { + if (!mHasUsedVoiceInput || + (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) { + // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel. + showVoiceWarningDialog(swipe); + } else { + reallyStartListening(swipe); + } + } + + private void reallyStartListening(boolean swipe) { + if (!mHasUsedVoiceInput) { + // The user has started a voice input, so remember that in the + // future (so we don't show the warning dialog after the first run). + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(this).edit(); + editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true); + editor.commit(); + mHasUsedVoiceInput = true; + } + + if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) { + // The user has started a voice input from an unsupported locale, so remember that + // in the future (so we don't show the warning dialog the next time they do this). + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(this).edit(); + editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true); + editor.commit(); + mHasUsedVoiceInputUnsupportedLocale = true; + } + + // Clear N-best suggestions + setSuggestions(null, false, false, true); + + FieldContext context = new FieldContext( + getCurrentInputConnection(), + getCurrentInputEditorInfo(), + mLanguageSwitcher.getInputLanguage(), + mLanguageSwitcher.getEnabledLanguages()); + mVoiceInput.startListening(context, swipe); + switchToRecognitionStatusView(); + } + + private void showVoiceWarningDialog(final boolean swipe) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + builder.setIcon(R.drawable.ic_mic_dialog); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + mVoiceInput.logKeyboardWarningDialogOk(); + reallyStartListening(swipe); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + mVoiceInput.logKeyboardWarningDialogCancel(); + } + }); + + if (mLocaleSupportedForVoiceInput) { + String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + + getString(R.string.voice_warning_how_to_turn_off); + builder.setMessage(message); + } else { + String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" + + getString(R.string.voice_warning_may_not_understand) + "\n\n" + + getString(R.string.voice_warning_how_to_turn_off); + builder.setMessage(message); + } + + builder.setTitle(R.string.voice_warning_title); + mVoiceWarningDialog = builder.create(); + + Window window = mVoiceWarningDialog.getWindow(); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.token = mInputView.getWindowToken(); + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; + window.setAttributes(lp); + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + mVoiceInput.logKeyboardWarningDialogShown(); + mVoiceWarningDialog.show(); + } + + public void onVoiceResults(List<String> candidates, + Map<String, List<CharSequence>> alternatives) { + if (!mRecognizing) { + return; + } + mVoiceResults.candidates = candidates; + mVoiceResults.alternatives = alternatives; + mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS)); + } + + private void handleVoiceResults() { + mAfterVoiceInput = true; + mImmediatelyAfterVoiceInput = true; + + InputConnection ic = getCurrentInputConnection(); + if (!isFullscreenMode()) { + // Start listening for updates to the text from typing, etc. + if (ic != null) { + ExtractedTextRequest req = new ExtractedTextRequest(); + ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); + } + } + + vibrate(); + switchToKeyboardView(); + + final List<CharSequence> nBest = new ArrayList<CharSequence>(); + boolean capitalizeFirstWord = preferCapitalization() + || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted()); + for (String c : mVoiceResults.candidates) { + if (capitalizeFirstWord) { + c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); + } + nBest.add(c); + } + + if (nBest.size() == 0) { + return; + } + + String bestResult = nBest.get(0).toString(); + + mVoiceInput.logVoiceInputDelivered(); + + mHints.registerVoiceResult(bestResult); + + if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text + + commitTyped(ic); + EditingUtil.appendText(ic, bestResult); + + if (ic != null) ic.endBatchEdit(); + + // Show N-Best alternates, if there is more than one choice. + if (nBest.size() > 1) { + mImmediatelyAfterVoiceSuggestions = true; + mShowingVoiceSuggestions = true; + setSuggestions(nBest.subList(1, nBest.size()), false, true, true); + setCandidatesViewShown(true); + } + mVoiceInputHighlighted = true; + mWordToSuggestions.putAll(mVoiceResults.alternatives); + + } + + private void setSuggestions( + List<CharSequence> suggestions, + boolean completions, + + boolean typedWordValid, + boolean haveMinimalSuggestion) { + + if (mIsShowingHint) { + setCandidatesView(mCandidateViewContainer); + mIsShowingHint = false; + } + + if (mCandidateView != null) { + mCandidateView.setSuggestions( + suggestions, completions, typedWordValid, haveMinimalSuggestion); + } + } + private void updateSuggestions() { + mSuggestionShouldReplaceCurrentWord = false; + // Check if we have a suggestion engine attached. - if (mSuggest == null || !isPredictionOn()) { + if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { return; } - + if (!mPredicting) { - mCandidateView.setSuggestions(null, false, false, false); + setNextSuggestions(); return; } @@ -817,7 +1363,7 @@ public class LatinIME extends InputMethodService // Don't auto-correct words with multiple capital letter correctionAvailable &= !mWord.isMostlyCaps(); - mCandidateView.setSuggestions(stringList, false, typedWordValid, correctionAvailable); + setSuggestions(stringList, false, typedWordValid, correctionAvailable); if (stringList.size() > 0) { if (correctionAvailable && !typedWordValid && stringList.size() > 1) { mBestWord = stringList.get(1); @@ -844,6 +1390,8 @@ public class LatinIME extends InputMethodService } public void pickSuggestionManually(int index, CharSequence suggestion) { + if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index); + if (mCompletionOn && mCompletions != null && index >= 0 && index < mCompletions.length) { CompletionInfo ci = mCompletions[index]; @@ -858,6 +1406,12 @@ public class LatinIME extends InputMethodService updateShiftKeyState(getCurrentInputEditorInfo()); return; } + + // If this is a punctuation, apply it through the normal key press + if (suggestion.length() == 1 && isWordSeparator(suggestion.charAt(0))) { + onKey(suggestion.charAt(0), null); + return; + } pickSuggestion(suggestion); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space @@ -867,18 +1421,23 @@ public class LatinIME extends InputMethodService // Fool the state watcher so that a subsequent backspace will not do a revert TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); } - + private void pickSuggestion(CharSequence suggestion) { if (mCapsLock) { suggestion = suggestion.toString().toUpperCase(); - } else if (preferCapitalization() + } else if (preferCapitalization() || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) { suggestion = suggestion.toString().toUpperCase().charAt(0) + suggestion.subSequence(1, suggestion.length()).toString(); } InputConnection ic = getCurrentInputConnection(); if (ic != null) { - ic.commitText(suggestion, 1); + if (mSuggestionShouldReplaceCurrentWord) { + EditingUtil.deleteWordAtCursor(ic, getWordSeparators()); + } + if (!VoiceInput.DELETE_SYMBOL.equals(suggestion)) { + ic.commitText(suggestion, 1); + } } // Add the word to the auto dictionary if it's not a known word if (mAutoDictionary.isValidWord(suggestion) || !mSuggest.isValidWord(suggestion)) { @@ -886,12 +1445,14 @@ public class LatinIME extends InputMethodService } mPredicting = false; mCommittedLength = suggestion.length(); - if (mCandidateView != null) { - mCandidateView.setSuggestions(null, false, false, false); - } + setNextSuggestions(); updateShiftKeyState(getCurrentInputEditorInfo()); } + private void setNextSuggestions() { + setSuggestions(mSuggestPuncList, false, false, false); + } + private boolean isCursorTouchingWord() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return false; @@ -901,13 +1462,13 @@ public class LatinIME extends InputMethodService && !isWordSeparator(toLeft.charAt(0))) { return true; } - if (!TextUtils.isEmpty(toRight) + if (!TextUtils.isEmpty(toRight) && !isWordSeparator(toRight.charAt(0))) { return true; } return false; } - + public void revertLastWord(boolean deleteChar) { final int length = mComposing.length(); if (!mPredicting && length > 0) { @@ -918,7 +1479,7 @@ public class LatinIME extends InputMethodService if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); - if (toTheLeft != null && toTheLeft.length() > 0 + if (toTheLeft != null && toTheLeft.length() > 0 && isWordSeparator(toTheLeft.charAt(0))) { toDelete--; } @@ -936,7 +1497,7 @@ public class LatinIME extends InputMethodService protected String getWordSeparators() { return mWordSeparators; } - + public boolean isWordSeparator(int code) { String separators = getWordSeparators(); return separators.contains(String.valueOf((char)code)); @@ -957,6 +1518,11 @@ public class LatinIME extends InputMethodService } public void swipeRight() { + if (userHasNotTypedRecently() && VOICE_INSTALLED && mEnableVoice && + fieldCanDoVoice(makeFieldContext())) { + startListening(true /* was a swipe */); + } + if (LatinKeyboardView.DEBUG_AUTO_PLAY) { ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); CharSequence text = cm.getText(); @@ -965,9 +1531,36 @@ public class LatinIME extends InputMethodService } } } - + + private void toggleLanguage(boolean reset, boolean next) { + if (reset) { + mLanguageSwitcher.reset(); + } else { + if (next) { + mLanguageSwitcher.next(); + } else { + mLanguageSwitcher.prev(); + } + } + int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode(); + reloadKeyboards(); + mKeyboardSwitcher.makeKeyboards(true); + mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0, + mEnableVoiceButton && mEnableVoice); + initSuggest(mLanguageSwitcher.getInputLanguage()); + mLanguageSwitcher.persist(); + updateShiftKeyState(getCurrentInputEditorInfo()); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (PREF_SELECTED_LANGUAGES.equals(key)) { + mLanguageSwitcher.loadLocales(sharedPreferences); + mRefreshKeyboardRequired = true; + } + } + public void swipeLeft() { - //handleBackspace(); } public void swipeDown() { @@ -984,9 +1577,37 @@ public class LatinIME extends InputMethodService } public void onRelease(int primaryCode) { + // Reset any drag flags in the keyboard + ((LatinKeyboard) mInputView.getKeyboard()).keyReleased(); //vibrate(); } + private FieldContext makeFieldContext() { + return new FieldContext( + getCurrentInputConnection(), + getCurrentInputEditorInfo(), + mLanguageSwitcher.getInputLanguage(), + mLanguageSwitcher.getEnabledLanguages()); + } + + private boolean fieldCanDoVoice(FieldContext fieldContext) { + return !mPasswordText + && mVoiceInput != null + && !mVoiceInput.isBlacklistedField(fieldContext); + } + + private boolean fieldIsRecommendedForVoice(FieldContext fieldContext) { + // TODO: Move this logic into the VoiceInput method. + return !mPasswordText && !mEmailText && mVoiceInput.isRecommendedField(fieldContext); + } + + private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) { + return ENABLE_VOICE_BUTTON + && fieldCanDoVoice(fieldContext) + && !(attribute != null && attribute.privateImeOptions != null + && attribute.privateImeOptions.equals(IME_OPTION_NO_MICROPHONE)); + } + // receive ringer mode changes to detect silent mode private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -1005,6 +1626,26 @@ public class LatinIME extends InputMethodService } } + private boolean userHasNotTypedRecently() { + return (SystemClock.uptimeMillis() - mLastKeyTime) + > MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE; + } + + /* + * Only trigger a swipe action if the user hasn't typed X millis before + * now, and if they don't type Y millis after the swipe is detected. This + * delays the onset of the swipe action by Y millis. + */ + private void conservativelyTriggerSwipeAction(final Runnable action) { + if (userHasNotTypedRecently()) { + mSwipeTriggerTimeMillis = System.currentTimeMillis(); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_START_LISTENING_AFTER_SWIPE), + MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING); + } + } + + private void playKeyClick(int primaryCode) { // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode @@ -1054,7 +1695,7 @@ public class LatinIME extends InputMethodService } } } - + private void startTutorial() { mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500); } @@ -1068,10 +1709,26 @@ public class LatinIME extends InputMethodService mUserDictionary.addWord(word, frequency); } - private void launchSettings() { + private void updateCorrectionMode() { + mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; + mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) + && !mInputTypeNoAutoCorrect && mHasDictionary; + mCorrectionMode = mAutoCorrectOn + ? Suggest.CORRECTION_FULL + : (mQuickFixes ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); + if (mSuggest != null) { + mSuggest.setCorrectionMode(mCorrectionMode); + } + } + + protected void launchSettings() { + launchSettings(LatinIMESettings.class); + } + + protected void launchSettings(Class settingsClass) { handleClose(); Intent intent = new Intent(); - intent.setClass(LatinIME.this, LatinIMESettings.class); + intent.setClass(LatinIME.this, settingsClass); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } @@ -1083,16 +1740,64 @@ public class LatinIME extends InputMethodService mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); + mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); + mHasUsedVoiceInputUnsupportedLocale = + sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false); + + // Get the current list of supported locales and check the current locale against that + // list. We cache this value so as not to check it every time the user starts a voice + // input. Because this method is called by onStartInputView, this should mean that as + // long as the locale doesn't change while the user is keeping the IME open, the + // value should never be stale. + String supportedLocalesString = SettingsUtil.getSettingsString( + getContentResolver(), + SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, + DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); + ArrayList<String> voiceInputSupportedLocales = + Lists.newArrayList(supportedLocalesString.split("\\s+")); + + mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mLocale); + // If there is no auto text data, then quickfix is forced to "on", so that the other options // will continue to work + if (AutoText.getSize(mInputView) < 1) mQuickFixes = true; mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true) & mQuickFixes; - boolean autoComplete = sp.getBoolean(PREF_AUTO_COMPLETE, - getResources().getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; - mAutoCorrectOn = mSuggest != null && (autoComplete || mQuickFixes); - mCorrectionMode = autoComplete - ? Suggest.CORRECTION_FULL - : (mQuickFixes ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); + + if (VOICE_INSTALLED) { + boolean enableVoice = sp.getBoolean(PREF_ENABLE_VOICE, true); + boolean voiceOnPrimary = sp.getBoolean(PREF_VOICE_MAIN, true); + if (mKeyboardSwitcher != null && + (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) { + mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary); + } + mEnableVoice = enableVoice; + mVoiceOnPrimary = voiceOnPrimary; + } + mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, + mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; + updateCorrectionMode(); + mLanguageSwitcher.loadLocales(sp); + } + + private String getPersistedInputLanguage() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + return sp.getString(PREF_INPUT_LANGUAGE, null); + } + + private String getSelectedInputLanguages() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + return sp.getString(PREF_SELECTED_LANGUAGES, null); + } + + private void initSuggestPuncList() { + mSuggestPuncList = new ArrayList<CharSequence>(); + String suggestPuncs = mResources.getString(R.string.suggested_punctuations); + if (suggestPuncs != null) { + for (int i = 0; i < suggestPuncs.length(); i++) { + mSuggestPuncList.add(suggestPuncs.subSequence(i, i + 1)); + } + } } private void showOptionsMenu() { @@ -1101,7 +1806,7 @@ public class LatinIME extends InputMethodService builder.setIcon(R.drawable.ic_dialog_keyboard); builder.setNegativeButton(android.R.string.cancel, null); CharSequence itemSettings = getString(R.string.english_ime_settings); - CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod); + CharSequence itemInputMethod = getString(R.string.inputMethod); builder.setItems(new CharSequence[] { itemSettings, itemInputMethod}, new DialogInterface.OnClickListener() { @@ -1119,7 +1824,7 @@ public class LatinIME extends InputMethodService } } }); - builder.setTitle(getResources().getString(R.string.english_ime_name)); + builder.setTitle(mResources.getString(R.string.english_ime_name)); mOptionsDialog = builder.create(); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); @@ -1138,10 +1843,10 @@ public class LatinIME extends InputMethodService updateShiftKeyState(getCurrentInputEditorInfo()); } - + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { super.dump(fd, fout, args); - + final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); @@ -1159,13 +1864,14 @@ public class LatinIME extends InputMethodService } // Characters per second measurement - + private static final boolean PERF_DEBUG = false; private long mLastCpsTime; private static final int CPS_BUFFER_SIZE = 16; private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; private int mCpsIndex; - + private boolean mInputTypeNoAutoCorrect; + private void measureCps() { if (!LatinIME.PERF_DEBUG) return; long now = System.currentTimeMillis(); @@ -1182,7 +1888,7 @@ public class LatinIME extends InputMethodService // If the user touches a typed word 2 times or more, it will become valid. private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED; // If the user touches a typed word 5 times or more, it will be added to the user dict. - private static final int PROMOTION_THRESHOLD = 5 * FREQUENCY_FOR_PICKED; + private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED; public AutoDictionary(Context context) { super(context); @@ -1191,7 +1897,7 @@ public class LatinIME extends InputMethodService @Override public boolean isValidWord(CharSequence word) { final int frequency = getWordFrequency(word); - return frequency > VALIDITY_THRESHOLD; + return frequency >= VALIDITY_THRESHOLD; } @Override @@ -1199,14 +1905,17 @@ public class LatinIME extends InputMethodService final int length = word.length(); // Don't add very short or very long words. if (length < 2 || length > getMaxWordLength()) return; - super.addWord(word, addFrequency); - final int freq = getWordFrequency(word); - if (freq > PROMOTION_THRESHOLD) { + if (mWord.isAutoCapitalized()) { + // Remove caps before adding + word = Character.toLowerCase(word.charAt(0)) + + word.substring(1); + } + int freq = getWordFrequency(word); + freq = freq < 0 ? addFrequency : freq + addFrequency; + super.addWord(word, freq); + if (freq >= PROMOTION_THRESHOLD) { LatinIME.this.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD); } } } } - - - diff --git a/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java b/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java index c454f120e..b6a800ebd 100644 --- a/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java +++ b/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java @@ -26,6 +26,6 @@ public class LatinIMEBackupAgent extends BackupHelperAgent { public void onCreate() { addHelper("shared_pref", new SharedPreferencesBackupHelper(this, - "com.android.inputmethod.latin_preferences")); + getPackageName() + "_preferences")); } } diff --git a/src/com/android/inputmethod/latin/LatinIMESettings.java b/src/com/android/inputmethod/latin/LatinIMESettings.java index c8ea309e3..98a0af9d1 100644 --- a/src/com/android/inputmethod/latin/LatinIMESettings.java +++ b/src/com/android/inputmethod/latin/LatinIMESettings.java @@ -1,12 +1,12 @@ /* * 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 @@ -16,32 +16,69 @@ package com.android.inputmethod.latin; +import android.app.AlertDialog; +import android.app.Dialog; import android.backup.BackupManager; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; +import android.preference.Preference.OnPreferenceClickListener; +import android.speech.RecognitionManager; import android.text.AutoText; +import android.util.Log; + +import com.google.android.collect.Lists; + +import com.android.inputmethod.voice.SettingsUtil; +import com.android.inputmethod.voice.VoiceInputLogger; + +import java.util.ArrayList; +import java.util.Locale; public class LatinIMESettings extends PreferenceActivity - implements SharedPreferences.OnSharedPreferenceChangeListener { + implements SharedPreferences.OnSharedPreferenceChangeListener, + OnPreferenceClickListener, + DialogInterface.OnDismissListener { private static final String QUICK_FIXES_KEY = "quick_fixes"; private static final String SHOW_SUGGESTIONS_KEY = "show_suggestions"; private static final String PREDICTION_SETTINGS_KEY = "prediction_settings"; - + private static final String VOICE_SETTINGS_KEY = "enable_voice_input"; + private static final String VOICE_SERVER_KEY = "voice_server_url"; + + private static final String TAG = "LatinIMESettings"; + + // Dialog ids + private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; + private CheckBoxPreference mQuickFixes; private CheckBoxPreference mShowSuggestions; - + private CheckBoxPreference mVoicePreference; + + private VoiceInputLogger mLogger; + + private boolean mOkClicked = false; + @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.prefs); mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY); mShowSuggestions = (CheckBoxPreference) findPreference(SHOW_SUGGESTIONS_KEY); - getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener( - this); + mVoicePreference = (CheckBoxPreference) findPreference(VOICE_SETTINGS_KEY); + + SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + prefs.registerOnSharedPreferenceChangeListener(this); + + mVoicePreference.setOnPreferenceClickListener(this); + mVoicePreference.setChecked(prefs.getBoolean( + VOICE_SETTINGS_KEY, getResources().getBoolean(R.bool.voice_input_default))); + + mLogger = VoiceInputLogger.getLogger(this); } @Override @@ -50,10 +87,17 @@ public class LatinIMESettings extends PreferenceActivity int autoTextSize = AutoText.getSize(getListView()); if (autoTextSize < 1) { ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY)) - .removePreference(mQuickFixes); + .removePreference(mQuickFixes); } else { mShowSuggestions.setDependency(QUICK_FIXES_KEY); } + if (!LatinIME.VOICE_INSTALLED + || !RecognitionManager.isRecognitionAvailable(this)) { + getPreferenceScreen().removePreference(mVoicePreference); + } + + mVoicePreference.setChecked( + getPreferenceManager().getSharedPreferences().getBoolean(VOICE_SETTINGS_KEY, true)); } @Override @@ -67,4 +111,91 @@ public class LatinIMESettings extends PreferenceActivity String key) { (new BackupManager(this)).dataChanged(); } + + public boolean onPreferenceClick(Preference preference) { + if (preference == mVoicePreference) { + if (mVoicePreference.isChecked()) { + mOkClicked = false; + showDialog(VOICE_INPUT_CONFIRM_DIALOG); + } else { + updateVoicePreference(); + } + } + return false; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case VOICE_INPUT_CONFIRM_DIALOG: + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + if (whichButton == DialogInterface.BUTTON_NEGATIVE) { + mVoicePreference.setChecked(false); + mLogger.settingsWarningDialogCancel(); + } else if (whichButton == DialogInterface.BUTTON_POSITIVE) { + mOkClicked = true; + mLogger.settingsWarningDialogOk(); + } + updateVoicePreference(); + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(R.string.voice_warning_title) + .setPositiveButton(android.R.string.ok, listener) + .setNegativeButton(android.R.string.cancel, listener); + + // Get the current list of supported locales and check the current locale against + // that list, to decide whether to put a warning that voice input will not work in + // the current language as part of the pop-up confirmation dialog. + String supportedLocalesString = SettingsUtil.getSettingsString( + getContentResolver(), + SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, + LatinIME.DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); + ArrayList<String> voiceInputSupportedLocales = + Lists.newArrayList(supportedLocalesString.split("\\s+")); + boolean localeSupported = voiceInputSupportedLocales.contains( + Locale.getDefault().toString()); + + if (localeSupported) { + String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + + getString(R.string.voice_hint_dialog_message); + builder.setMessage(message); + } else { + String message = getString(R.string.voice_warning_locale_not_supported) + + "\n\n" + getString(R.string.voice_warning_may_not_understand) + "\n\n" + + getString(R.string.voice_hint_dialog_message); + builder.setMessage(message); + } + + AlertDialog dialog = builder.create(); + dialog.setOnDismissListener(this); + mLogger.settingsWarningDialogShown(); + return dialog; + default: + Log.e(TAG, "unknown dialog " + id); + return null; + } + } + + public void onDismiss(DialogInterface dialog) { + mLogger.settingsWarningDialogDismissed(); + if (!mOkClicked) { + // This assumes that onPreferenceClick gets called first, and this if the user + // agreed after the warning, we set the mOkClicked value to true. + mVoicePreference.setChecked(false); + } + } + + private void updateVoicePreference() { + SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); + boolean isChecked = mVoicePreference.isChecked(); + if (isChecked) { + mLogger.voiceInputSettingEnabled(); + } else { + mLogger.voiceInputSettingDisabled(); + } + editor.putBoolean(VOICE_SETTINGS_KEY, isChecked); + editor.commit(); + } } diff --git a/src/com/android/inputmethod/latin/LatinKeyboard.java b/src/com/android/inputmethod/latin/LatinKeyboard.java index 9b04aa264..27c409a03 100644 --- a/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -16,11 +16,26 @@ package com.android.inputmethod.latin; +import java.util.List; +import java.util.Locale; + import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Paint.Align; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.inputmethodservice.Keyboard; +import android.text.TextPaint; +import android.view.ViewConfiguration; import android.view.inputmethod.EditorInfo; public class LatinKeyboard extends Keyboard { @@ -29,8 +44,33 @@ public class LatinKeyboard extends Keyboard { private Drawable mShiftLockPreviewIcon; private Drawable mOldShiftIcon; private Drawable mOldShiftPreviewIcon; + private Drawable mSpaceIcon; + private Drawable mSpacePreviewIcon; + private Drawable mMicIcon; + private Drawable mMicPreviewIcon; + private Drawable m123MicIcon; + private Drawable m123MicPreviewIcon; + private Drawable mButtonArrowLeftIcon; + private Drawable mButtonArrowRightIcon; private Key mShiftKey; private Key mEnterKey; + private Key mF1Key; + private Key mSpaceKey; + private Key m123Key; + private int mSpaceKeyIndex = -1; + private int mSpaceDragStartX; + private int mSpaceDragLastDiff; + /* package */ Locale mLocale; + private LanguageSwitcher mLanguageSwitcher; + private Resources mRes; + private Context mContext; + private int mMode; + private boolean mHasVoice; + private boolean mCurrentlyInSpace; + private SlidingLocaleDrawable mSlidingLocaleIcon; + private Rect mBounds = new Rect(); + + private int mExtensionResId; private static final int SHIFT_OFF = 0; private static final int SHIFT_ON = 1; @@ -38,22 +78,40 @@ public class LatinKeyboard extends Keyboard { private int mShiftState = SHIFT_OFF; + private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; + static int sSpacebarVerticalCorrection; public LatinKeyboard(Context context, int xmlLayoutResId) { - this(context, xmlLayoutResId, 0); + this(context, xmlLayoutResId, 0, false); } - public LatinKeyboard(Context context, int xmlLayoutResId, int mode) { + public LatinKeyboard(Context context, int xmlLayoutResId, int mode, boolean hasVoice) { super(context, xmlLayoutResId, mode); - Resources res = context.getResources(); + final Resources res = context.getResources(); + mContext = context; + mMode = mode; + mRes = res; + mHasVoice = hasVoice; mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); mShiftLockPreviewIcon.setBounds(0, 0, mShiftLockPreviewIcon.getIntrinsicWidth(), mShiftLockPreviewIcon.getIntrinsicHeight()); + mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); + mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space); + mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic); + mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic); + setDefaultBounds(mMicPreviewIcon); + mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); + mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); + m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic); + m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic); + setDefaultBounds(m123MicPreviewIcon); sSpacebarVerticalCorrection = res.getDimensionPixelOffset( R.dimen.spacebar_vertical_correction); + setF1Key(xmlLayoutResId == R.xml.kbd_qwerty); + mSpaceKeyIndex = indexOf((int) ' '); } public LatinKeyboard(Context context, int layoutTemplateResId, @@ -65,12 +123,23 @@ public class LatinKeyboard extends Keyboard { protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { Key key = new LatinKey(res, parent, x, y, parser); - if (key.codes[0] == 10) { + switch (key.codes[0]) { + case 10: mEnterKey = key; + break; + case LatinKeyboardView.KEYCODE_F1: + mF1Key = key; + break; + case 32: + mSpaceKey = key; + break; + case KEYCODE_MODE_CHANGE: + m123Key = key; + break; } return key; } - + void setImeOptions(Resources res, int mode, int options) { if (mEnterKey != null) { // Reset some of the rarely used attributes. @@ -181,7 +250,7 @@ public class LatinKeyboard extends Keyboard { } return shiftChanged; } - + @Override public boolean isShifted() { if (mShiftKey != null) { @@ -191,7 +260,220 @@ public class LatinKeyboard extends Keyboard { } } - static class LatinKey extends Keyboard.Key { + public void setExtension(int resId) { + mExtensionResId = resId; + } + + public int getExtension() { + return mExtensionResId; + } + + private void setDefaultBounds(Drawable drawable) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + + private void setF1Key(boolean isAlphaKeyboard) { + if (mF1Key == null) return; + if (!mHasVoice) { + mF1Key.label = ","; + mF1Key.codes = new int[] { ',' }; + mF1Key.icon = null; + mF1Key.iconPreview = null; + if (isAlphaKeyboard && m123Key != null) { + m123Key.icon = m123MicIcon; + m123Key.iconPreview = m123MicPreviewIcon; + m123Key.label = null; + } + } else { + mF1Key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE }; + mF1Key.label = null; + mF1Key.icon = mMicIcon; + mF1Key.iconPreview = mMicPreviewIcon; + } + } + + private void updateSpaceBarForLocale() { + if (mLocale != null) { + // Create the graphic for spacebar + Bitmap buffer = Bitmap.createBitmap(mSpaceKey.width, mSpaceIcon.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(buffer); + canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + Paint paint = new Paint(); + paint.setAntiAlias(true); + // Get the text size from the theme + paint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14)); + paint.setTextAlign(Align.CENTER); + // Draw a drop shadow for the text + paint.setShadowLayer(2f, 0, 0, 0xFF000000); + paint.setColor(0xFF808080); + final String language = getInputLanguage(mSpaceKey.width, paint); + final int ascent = (int) -paint.ascent(); + canvas.drawText(language, + buffer.getWidth() / 2, ascent, paint); + // Put arrows on either side of the text + if (mLanguageSwitcher.getLocaleCount() > 1) { + Rect bounds = new Rect(); + paint.getTextBounds(language, 0, language.length(), bounds); + drawButtonArrow(mButtonArrowLeftIcon, canvas, + (mSpaceKey.width - bounds.right) / 2 + - mButtonArrowLeftIcon.getIntrinsicWidth(), + (int) paint.getTextSize()); + drawButtonArrow(mButtonArrowRightIcon, canvas, + (mSpaceKey.width + bounds.right) / 2, (int) paint.getTextSize()); + } + // Draw the spacebar icon at the bottom + int x = (buffer.getWidth() - mSpaceIcon.getIntrinsicWidth()) / 2; + int y = buffer.getHeight() - mSpaceIcon.getIntrinsicHeight(); + mSpaceIcon.setBounds(x, y, + x + mSpaceIcon.getIntrinsicWidth(), y + mSpaceIcon.getIntrinsicHeight()); + mSpaceIcon.draw(canvas); + mSpaceKey.icon = new BitmapDrawable(mRes, buffer); + mSpaceKey.repeatable = mLanguageSwitcher.getLocaleCount() < 2; + } else { + mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space); + mSpaceKey.repeatable = true; + } + } + + private void drawButtonArrow(Drawable arrow, Canvas canvas, int x, int bottomY) { + arrow.setBounds(x, bottomY - arrow.getIntrinsicHeight(), x + arrow.getIntrinsicWidth(), + bottomY); + arrow.draw(canvas); + } + + private String getInputLanguage(int widthAvail, Paint paint) { + return chooseDisplayName(mLanguageSwitcher.getInputLocale(), widthAvail, paint); + } + + private String getNextInputLanguage(int widthAvail, Paint paint) { + return chooseDisplayName(mLanguageSwitcher.getNextInputLocale(), widthAvail, paint); + } + + private String getPrevInputLanguage(int widthAvail, Paint paint) { + return chooseDisplayName(mLanguageSwitcher.getPrevInputLocale(), widthAvail, paint); + } + + private String chooseDisplayName(Locale locale, int widthAvail, Paint paint) { + if (widthAvail < (int) (.35 * getMinWidth())) { + return locale.getLanguage().substring(0, 2).toUpperCase(locale); + } else { + return locale.getDisplayLanguage(locale); + } + } + + private void updateLocaleDrag(int diff) { + if (mSlidingLocaleIcon == null) { + mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, mSpaceKey.width, + mSpacePreviewIcon.getIntrinsicHeight()); + mSlidingLocaleIcon.setBounds(0, 0, mSpaceKey.width, + mSpacePreviewIcon.getIntrinsicHeight()); + mSpaceKey.iconPreview = mSlidingLocaleIcon; + } + mSlidingLocaleIcon.setDiff(diff); + if (Math.abs(diff) == Integer.MAX_VALUE) { + mSpaceKey.iconPreview = mSpacePreviewIcon; + } else { + mSpaceKey.iconPreview = mSlidingLocaleIcon; + } + mSpaceKey.iconPreview.invalidateSelf(); + } + + public int getLanguageChangeDirection() { + if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2 + || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) { + return 0; // No change + } + return mSpaceDragLastDiff > 0 ? 1 : -1; + } + + public void setLanguageSwitcher(LanguageSwitcher switcher) { + mLanguageSwitcher = switcher; + Locale locale = mLanguageSwitcher.getLocaleCount() > 0 + ? mLanguageSwitcher.getInputLocale() + : null; + if (mLocale != null && mLocale.equals(locale)) return; + mLocale = locale; + updateSpaceBarForLocale(); + } + + boolean isCurrentlyInSpace() { + return mCurrentlyInSpace; + } + + void keyReleased() { + mCurrentlyInSpace = false; + mSpaceDragLastDiff = 0; + if (mSpaceKey != null) { + updateLocaleDrag(Integer.MAX_VALUE); + } + } + + /** + * Does the magic of locking the touch gesture into the spacebar when + * switching input languages. + */ + boolean isInside(LatinKey key, int x, int y) { + final int code = key.codes[0]; + if (code == KEYCODE_SHIFT || + code == KEYCODE_DELETE) { + y -= key.height / 10; + if (code == KEYCODE_SHIFT) x += key.width / 6; + if (code == KEYCODE_DELETE) x -= key.width / 6; + } else if (code == LatinIME.KEYCODE_SPACE) { + y += LatinKeyboard.sSpacebarVerticalCorrection; + if (mLanguageSwitcher.getLocaleCount() > 1) { + if (mCurrentlyInSpace) { + int diff = x - mSpaceDragStartX; + if (Math.abs(diff - mSpaceDragLastDiff) > 0) { + updateLocaleDrag(diff); + } + mSpaceDragLastDiff = diff; + return true; + } else { + boolean insideSpace = key.isInsideSuper(x, y); + if (insideSpace) { + mCurrentlyInSpace = true; + mSpaceDragStartX = x; + updateLocaleDrag(0); + } + return insideSpace; + } + } + } + + // Lock into the spacebar + if (mCurrentlyInSpace) return false; + + return key.isInsideSuper(x, y); + } + + @Override + public int[] getNearestKeys(int x, int y) { + if (mCurrentlyInSpace) { + return new int[] { mSpaceKeyIndex }; + } else { + return super.getNearestKeys(x, y); + } + } + + private int indexOf(int code) { + List<Key> keys = getKeys(); + int count = keys.size(); + for (int i = 0; i < count; i++) { + if (keys.get(i).codes[0] == code) return i; + } + return -1; + } + + private int getTextSizeFromTheme(int style, int defValue) { + TypedArray array = mContext.getTheme().obtainStyledAttributes( + style, new int[] { android.R.attr.textSize }); + int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue); + return textSize; + } + + class LatinKey extends Keyboard.Key { private boolean mShiftLockEnabled; @@ -222,16 +504,125 @@ public class LatinKeyboard extends Keyboard { */ @Override public boolean isInside(int x, int y) { - final int code = codes[0]; - if (code == KEYCODE_SHIFT || - code == KEYCODE_DELETE) { - y -= height / 10; - if (code == KEYCODE_SHIFT) x += width / 6; - if (code == KEYCODE_DELETE) x -= width / 6; - } else if (code == LatinIME.KEYCODE_SPACE) { - y += LatinKeyboard.sSpacebarVerticalCorrection; - } + return LatinKeyboard.this.isInside(this, x, y); + } + + boolean isInsideSuper(int x, int y) { return super.isInside(x, y); } } + + /** + * Animation to be displayed on the spacebar preview popup when switching + * languages by swiping the spacebar. It draws the current, previous and + * next languages and moves them by the delta of touch movement on the spacebar. + */ + class SlidingLocaleDrawable extends Drawable { + + private int mWidth; + private int mHeight; + private Drawable mBackground; + private int mDiff; + private TextPaint mTextPaint; + private int mMiddleX; + private int mAscent; + private Drawable mLeftDrawable; + private Drawable mRightDrawable; + private boolean mHitThreshold; + private int mThreshold; + private String mCurrentLanguage; + private String mNextLanguage; + private String mPrevLanguage; + + public SlidingLocaleDrawable(Drawable background, int width, int height) { + mBackground = background; + mBackground.setBounds(0, 0, + mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight()); + mWidth = width; + mHeight = height; + mTextPaint = new TextPaint(); + int textSize = getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18); + mTextPaint.setTextSize(textSize); + mTextPaint.setColor(0); + mTextPaint.setTextAlign(Align.CENTER); + mTextPaint.setAlpha(255); + mTextPaint.setAntiAlias(true); + mAscent = (int) mTextPaint.ascent(); + mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2; + mLeftDrawable = + mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left); + mRightDrawable = + mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right); + mLeftDrawable.setBounds(0, 0, + mLeftDrawable.getIntrinsicWidth(), mLeftDrawable.getIntrinsicHeight()); + mRightDrawable.setBounds(mWidth - mRightDrawable.getIntrinsicWidth(), 0, + mWidth, mRightDrawable.getIntrinsicHeight()); + mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop(); + } + + void setDiff(int diff) { + if (diff == Integer.MAX_VALUE) { + mHitThreshold = false; + mCurrentLanguage = null; + return; + } + mDiff = diff; + if (mDiff > mWidth) mDiff = mWidth; + if (mDiff < -mWidth) mDiff = -mWidth; + if (Math.abs(mDiff) > mThreshold) mHitThreshold = true; + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + if (mHitThreshold) { + mTextPaint.setColor(0xFF000000); + canvas.clipRect(0, 0, mWidth, mHeight); + if (mCurrentLanguage == null) { + mCurrentLanguage = getInputLanguage(mWidth, mTextPaint); + mNextLanguage = getNextInputLanguage(mWidth, mTextPaint); + mPrevLanguage = getPrevInputLanguage(mWidth, mTextPaint); + } + canvas.drawText(mCurrentLanguage, + mWidth / 2 + mDiff, -mAscent + 4, mTextPaint); + canvas.drawText(mNextLanguage, + mDiff - mWidth / 2, -mAscent + 4, mTextPaint); + canvas.drawText(mPrevLanguage, + mDiff + mWidth + mWidth / 2, -mAscent + 4, mTextPaint); + mLeftDrawable.draw(canvas); + mRightDrawable.draw(canvas); + } + if (mBackground != null) { + canvas.translate(mMiddleX, 0); + mBackground.draw(canvas); + } + canvas.restore(); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + // Ignore + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Ignore + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + } } diff --git a/src/com/android/inputmethod/latin/LatinKeyboardView.java b/src/com/android/inputmethod/latin/LatinKeyboardView.java index d9ff0aa8c..05f8aff36 100644 --- a/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import java.util.List; + import android.content.Context; import android.graphics.Canvas; import android.inputmethodservice.Keyboard; @@ -25,17 +27,26 @@ import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.MotionEvent; - -import java.util.List; +import android.widget.PopupWindow; public class LatinKeyboardView extends KeyboardView { static final int KEYCODE_OPTIONS = -100; static final int KEYCODE_SHIFT_LONGPRESS = -101; + static final int KEYCODE_VOICE = -102; + static final int KEYCODE_F1 = -103; + static final int KEYCODE_NEXT_LANGUAGE = -104; + static final int KEYCODE_PREV_LANGUAGE = -105; private Keyboard mPhoneKeyboard; + private boolean mExtensionVisible; + private LatinKeyboardView mExtension; + private PopupWindow mExtensionPopup; + private boolean mFirstEvent; + public LatinKeyboardView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -66,7 +77,129 @@ public class LatinKeyboardView extends KeyboardView { } } - + @Override + public boolean onTouchEvent(MotionEvent me) { + LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); + // Reset any bounding box controls in the keyboard + if (me.getAction() == MotionEvent.ACTION_DOWN) { + keyboard.keyReleased(); + } + + if (me.getAction() == MotionEvent.ACTION_UP) { + int languageDirection = keyboard.getLanguageChangeDirection(); + if (languageDirection != 0) { + getOnKeyboardActionListener().onKey( + languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE, + null); + me.setAction(MotionEvent.ACTION_CANCEL); + keyboard.keyReleased(); + return super.onTouchEvent(me); + } + } + + // If we don't have an extension keyboard, don't go any further. + if (keyboard.getExtension() == 0) { + return super.onTouchEvent(me); + } + if (me.getY() < 0) { + if (mExtensionVisible) { + int action = me.getAction(); + if (mFirstEvent) action = MotionEvent.ACTION_DOWN; + mFirstEvent = false; + MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), + action, + me.getX(), me.getY() + mExtension.getHeight(), me.getMetaState()); + boolean result = mExtension.onTouchEvent(translated); + translated.recycle(); + if (me.getAction() == MotionEvent.ACTION_UP + || me.getAction() == MotionEvent.ACTION_CANCEL) { + closeExtension(); + } + return result; + } else { + if (openExtension()) { + MotionEvent cancel = MotionEvent.obtain(me.getDownTime(), me.getEventTime(), + MotionEvent.ACTION_CANCEL, me.getX() - 100, me.getY() - 100, 0); + super.onTouchEvent(cancel); + cancel.recycle(); + if (mExtension.getHeight() > 0) { + MotionEvent translated = MotionEvent.obtain(me.getEventTime(), + me.getEventTime(), + MotionEvent.ACTION_DOWN, + me.getX(), me.getY() + mExtension.getHeight(), + me.getMetaState()); + mExtension.onTouchEvent(translated); + translated.recycle(); + } else { + mFirstEvent = true; + } + } + return true; + } + } else if (mExtensionVisible) { + closeExtension(); + // Send a down event into the main keyboard first + MotionEvent down = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), + MotionEvent.ACTION_DOWN, + me.getX(), me.getY(), me.getMetaState()); + super.onTouchEvent(down); + down.recycle(); + // Send the actual event + return super.onTouchEvent(me); + } else { + return super.onTouchEvent(me); + } + } + + private boolean openExtension() { + if (((LatinKeyboard) getKeyboard()).getExtension() == 0) return false; + makePopupWindow(); + mExtensionVisible = true; + return true; + } + + private void makePopupWindow() { + if (mExtensionPopup == null) { + int[] windowLocation = new int[2]; + mExtensionPopup = new PopupWindow(getContext()); + mExtensionPopup.setBackgroundDrawable(null); + LayoutInflater li = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mExtension = (LatinKeyboardView) li.inflate(R.layout.input, null); + mExtension.setOnKeyboardActionListener((LatinIME) getContext()); + mExtension.setPopupParent(this); + mExtension.setPopupOffset(0, -windowLocation[1]); + Keyboard keyboard; + mExtension.setKeyboard(keyboard = new LatinKeyboard(getContext(), + ((LatinKeyboard) getKeyboard()).getExtension())); + mExtensionPopup.setContentView(mExtension); + mExtensionPopup.setWidth(getWidth()); + mExtensionPopup.setHeight(keyboard.getHeight()); + getLocationInWindow(windowLocation); + // TODO: Fix the "- 30". + mExtension.setPopupOffset(0, -windowLocation[1] - 30); + mExtensionPopup.showAtLocation(this, 0, 0, -keyboard.getHeight() + + windowLocation[1]); + } else { + mExtension.setVisibility(VISIBLE); + } + } + + @Override + public void closing() { + super.closing(); + if (mExtensionPopup != null && mExtensionPopup.isShowing()) { + mExtensionPopup.dismiss(); + mExtensionPopup = null; + } + } + + private void closeExtension() { + mExtension.setVisibility(INVISIBLE); + mExtension.closing(); + mExtensionVisible = false; + } + /**************************** INSTRUMENTATION *******************************/ static final boolean DEBUG_AUTO_PLAY = false; diff --git a/src/com/android/inputmethod/latin/Suggest.java b/src/com/android/inputmethod/latin/Suggest.java index c025566b7..c3fe99635 100755 --- a/src/com/android/inputmethod/latin/Suggest.java +++ b/src/com/android/inputmethod/latin/Suggest.java @@ -26,6 +26,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import com.android.inputmethod.latin.WordComposer; + /** * This class loads a dictionary and provides a list of suggestions for a given sequence of * characters. This includes corrections and completions. @@ -37,7 +39,9 @@ public class Suggest implements Dictionary.WordCallback { public static final int CORRECTION_BASIC = 1; public static final int CORRECTION_FULL = 2; - private Dictionary mMainDict; + private static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; + + private BinaryDictionary mMainDict; private Dictionary mUserDictionary; @@ -49,18 +53,16 @@ public class Suggest implements Dictionary.WordCallback { private int[] mPriorities = new int[mPrefMaxSuggestions]; private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); - private boolean mIncludeTypedWordIfValid; private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); - private Context mContext; private boolean mHaveCorrection; private CharSequence mOriginalWord; private String mLowerOriginalWord; + private boolean mCapitalize; private int mCorrectionMode = CORRECTION_BASIC; public Suggest(Context context, int dictionaryResId) { - mContext = context; mMainDict = new BinaryDictionary(context, dictionaryResId); for (int i = 0; i < mPrefMaxSuggestions; i++) { StringBuilder sb = new StringBuilder(32); @@ -76,6 +78,10 @@ public class Suggest implements Dictionary.WordCallback { mCorrectionMode = mode; } + public boolean hasMainDictionary() { + return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; + } + /** * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted * before the main dictionary, if set. @@ -153,9 +159,9 @@ public class Suggest implements Dictionary.WordCallback { public List<CharSequence> getSuggestions(View view, WordComposer wordComposer, boolean includeTypedWordIfValid) { mHaveCorrection = false; + mCapitalize = wordComposer.isCapitalized(); collectGarbage(); Arrays.fill(mPriorities, 0); - mIncludeTypedWordIfValid = includeTypedWordIfValid; // Save a lowercase version of the original word mOriginalWord = wordComposer.getTypedWord(); @@ -298,7 +304,14 @@ public class Suggest implements Dictionary.WordCallback { StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) : new StringBuilder(32); sb.setLength(0); - sb.append(word, offset, length); + if (mCapitalize) { + sb.append(Character.toUpperCase(word[offset])); + if (length > 1) { + sb.append(word, offset + 1, length - 1); + } + } else { + sb.append(word, offset, length); + } mSuggestions.add(pos, sb); if (mSuggestions.size() > prefMaxSuggestions) { CharSequence garbage = mSuggestions.remove(prefMaxSuggestions); @@ -336,4 +349,10 @@ public class Suggest implements Dictionary.WordCallback { } mSuggestions.clear(); } + + public void close() { + if (mMainDict != null) { + mMainDict.close(); + } + } } diff --git a/src/com/android/inputmethod/latin/TextEntryState.java b/src/com/android/inputmethod/latin/TextEntryState.java index 90c364a1c..c5e8ad9a1 100644 --- a/src/com/android/inputmethod/latin/TextEntryState.java +++ b/src/com/android/inputmethod/latin/TextEntryState.java @@ -123,6 +123,7 @@ public class TextEntryState { } public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) { + if (typedWord == null) return; if (!typedWord.equals(actualWord)) { sAutoSuggestCount++; } diff --git a/src/com/android/inputmethod/latin/WordComposer.java b/src/com/android/inputmethod/latin/WordComposer.java index 50725d481..e97cb24ba 100644 --- a/src/com/android/inputmethod/latin/WordComposer.java +++ b/src/com/android/inputmethod/latin/WordComposer.java @@ -36,6 +36,8 @@ public class WordComposer { private StringBuilder mTypedWord; private int mCapsCount; + + private boolean mAutoCapitalized; /** * Whether the user chose to capitalize the word. @@ -152,4 +154,21 @@ public class WordComposer { public boolean isMostlyCaps() { return mCapsCount > 1; } + + /** + * Saves the reason why the word is capitalized - whether it was automatic or + * due to the user hitting shift in the middle of a sentence. + * @param auto whether it was an automatic capitalization due to start of sentence + */ + public void setAutoCapitalized(boolean auto) { + mAutoCapitalized = auto; + } + + /** + * Returns whether the word was automatically capitalized. + * @return whether the word was automatically capitalized + */ + public boolean isAutoCapitalized() { + return mAutoCapitalized; + } } diff --git a/src/com/android/inputmethod/voice/EditingUtil.java b/src/com/android/inputmethod/voice/EditingUtil.java new file mode 100644 index 000000000..6316d8ccf --- /dev/null +++ b/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/src/com/android/inputmethod/voice/FieldContext.java b/src/com/android/inputmethod/voice/FieldContext.java new file mode 100644 index 000000000..5fbacfb6c --- /dev/null +++ b/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/src/com/android/inputmethod/voice/LatinIMEWithVoice.java b/src/com/android/inputmethod/voice/LatinIMEWithVoice.java new file mode 100644 index 000000000..ccbf5b6bc --- /dev/null +++ b/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/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java b/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java new file mode 100644 index 000000000..13a58e14d --- /dev/null +++ b/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/src/com/android/inputmethod/voice/RecognitionView.java b/src/com/android/inputmethod/voice/RecognitionView.java new file mode 100644 index 000000000..fd3d6d0de --- /dev/null +++ b/src/com/android/inputmethod/voice/RecognitionView.java @@ -0,0 +1,321 @@ +/* + * 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.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.TextView; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.voice.SettingsUtil; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * 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"; + + // If there's a significant delay between starting up voice search and the + // onset of audio recording, show the "initializing" screen first. If not, + // jump directly to the "speak now" screen to avoid flashing "initializing" + // quickly. + private static final boolean EXPECT_RECORDING_DELAY = true; + + 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 showInitializing() { + mUiHandler.post(new Runnable() { + public void run() { + mText.setText(R.string.voice_initializing); + mImage.setImageDrawable(mInitializing); + mButtonText.setText(mContext.getText(R.string.cancel)); + } + }); + } + + public void showStartState() { + if (EXPECT_RECORDING_DELAY) { + showInitializing(); + } else { + showListening(); + } + } + + public void showListening() { + mState = State.LISTENING; + mUiHandler.post(new Runnable() { + public void run() { + mText.setText(R.string.voice_listening); + mImage.setImageDrawable(mSpeakNow.get(0)); + mButtonText.setText(mContext.getText(R.string.cancel)); + } + }); + mUiHandler.postDelayed(mUpdateVolumeRunnable, 50); + } + + public void updateVoiceMeter(final float rmsdB) { + mVolume = rmsdB; + } + + public void showError(final String message) { + mState = State.READY; + mUiHandler.post(new Runnable() { + public void run() { + exitWorking(); + mText.setText(message); + mImage.setImageDrawable(mError); + mButtonText.setText(mContext.getText(R.string.ok)); + } + }); + } + + public void showWorking( + final ByteArrayOutputStream waveBuffer, + final int speechStartPosition, + final int speechEndPosition) { + + mState = State.WORKING; + + mUiHandler.post(new Runnable() { + public void run() { + mText.setText(R.string.voice_working); + mImage.setVisibility(View.GONE); + mProgress.setVisibility(View.VISIBLE); + final ShortBuffer buf = ByteBuffer.wrap(waveBuffer.toByteArray()) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + buf.position(0); + waveBuffer.reset(); + showWave(buf, speechStartPosition / 2, speechEndPosition / 2); + } + }); + } + + /** + * @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() { + mState = State.READY; + mUiHandler.post(new Runnable() { + public void run() { + exitWorking(); + } + }); + showStartState(); + } + + private void exitWorking() { + mProgress.setVisibility(View.GONE); + mImage.setVisibility(View.VISIBLE); + } +} diff --git a/src/com/android/inputmethod/voice/SettingsUtil.java b/src/com/android/inputmethod/voice/SettingsUtil.java new file mode 100644 index 000000000..abf52047f --- /dev/null +++ b/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/src/com/android/inputmethod/voice/VoiceInput.java b/src/com/android/inputmethod/voice/VoiceInput.java new file mode 100644 index 000000000..a2e210536 --- /dev/null +++ b/src/com/android/inputmethod/voice/VoiceInput.java @@ -0,0 +1,500 @@ +/* + * 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"); + } + + /** + * @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/src/com/android/inputmethod/voice/VoiceInputLogger.java b/src/com/android/inputmethod/voice/VoiceInputLogger.java new file mode 100644 index 000000000..659033340 --- /dev/null +++ b/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/src/com/android/inputmethod/voice/WaveformImage.java b/src/com/android/inputmethod/voice/WaveformImage.java new file mode 100644 index 000000000..08d87c8f3 --- /dev/null +++ b/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/src/com/android/inputmethod/voice/Whitelist.java b/src/com/android/inputmethod/voice/Whitelist.java new file mode 100644 index 000000000..167b688ca --- /dev/null +++ b/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; + } +} diff --git a/src/com/google/android/voicesearch/LatinIMEWithVoice.java b/src/com/google/android/voicesearch/LatinIMEWithVoice.java new file mode 100644 index 000000000..8a339d14a --- /dev/null +++ b/src/com/google/android/voicesearch/LatinIMEWithVoice.java @@ -0,0 +1,29 @@ +/* + * + * 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.google.android.voicesearch; + +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/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java b/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java new file mode 100644 index 000000000..a53cebfd9 --- /dev/null +++ b/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java @@ -0,0 +1,5 @@ +package com.google.android.voicesearch; + +import com.android.inputmethod.latin.LatinIMESettings; + +public class LatinIMEWithVoiceSettings extends LatinIMESettings {} |