diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
24 files changed, 2081 insertions, 1561 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java index 485ec511f..e5eb15938 100644 --- a/java/src/com/android/inputmethod/latin/AutoCorrection.java +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -98,7 +98,7 @@ public class AutoCorrection { return whiteListedWord != null; } - private boolean hasAutoCorrectionForTypedWord(Map<String, Dictionary> dictionaries, + private static boolean hasAutoCorrectionForTypedWord(Map<String, Dictionary> dictionaries, WordComposer wordComposer, ArrayList<CharSequence> suggestions, CharSequence typedWord, int correctionMode) { if (TextUtils.isEmpty(typedWord)) return false; @@ -118,8 +118,9 @@ public class AutoCorrection { final int autoCorrectionSuggestionScore = sortedScores[0]; // TODO: when the normalized score of the first suggestion is nearly equals to // the normalized score of the second suggestion, behave less aggressive. - mNormalizedScore = Utils.calcNormalizedScore( - typedWord,autoCorrectionSuggestion, autoCorrectionSuggestionScore); + mNormalizedScore = BinaryDictionary.calcNormalizedScore( + typedWord.toString(), autoCorrectionSuggestion.toString(), + autoCorrectionSuggestionScore); if (DBG) { Log.d(TAG, "Normalized " + typedWord + "," + autoCorrectionSuggestion + "," + autoCorrectionSuggestionScore + ", " + mNormalizedScore diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index b9fd57434..3692ac179 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -46,7 +46,7 @@ public class BinaryDictionary extends Dictionary { private static final int TYPED_LETTER_MULTIPLIER = 2; private int mDicTypeId; - private int mNativeDict; + private long mNativeDict; private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_PROXIMITY_CHARS_SIZE]; private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; @@ -107,17 +107,21 @@ public class BinaryDictionary extends Dictionary { Utils.loadNativeLibrary(); } - private native int openNative(String sourceDir, long dictOffset, long dictSize, + private native long openNative(String sourceDir, long dictOffset, long dictSize, int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords, int maxAlternatives); - private native void closeNative(int dict); - private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); - private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates, + private native void closeNative(long dict); + private native boolean isValidWordNative(long dict, char[] word, int wordLength); + private native int getSuggestionsNative(long dict, long proximityInfo, int[] xCoordinates, int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars, int[] scores); - private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, + private native int getBigramsNative(long dict, char[] prevWord, int prevWordLength, int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores, int maxWordLength, int maxBigrams, int maxAlternatives); + private static native double calcNormalizedScoreNative( + char[] before, int beforeLength, char[] after, int afterLength, int score); + private static native int editDistanceNative( + char[] before, int beforeLength, char[] after, int afterLength); private final void loadDictionary(String path, long startOffset, long length) { mNativeDict = openNative(path, startOffset, length, @@ -211,6 +215,16 @@ public class BinaryDictionary extends Dictionary { mFlags, outputChars, scores); } + public static double calcNormalizedScore(String before, String after, int score) { + return calcNormalizedScoreNative(before.toCharArray(), before.length(), + after.toCharArray(), after.length(), score); + } + + public static int editDistance(String before, String after) { + return editDistanceNative( + before.toCharArray(), before.length(), after.toCharArray(), after.length()); + } + @Override public boolean isValidWord(CharSequence word) { if (word == null) return false; diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index 739153044..c19a5a718 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -18,6 +18,8 @@ package com.android.inputmethod.latin; import com.android.inputmethod.keyboard.ProximityInfo; +import android.util.Log; + import java.util.Collection; import java.util.Collections; import java.util.List; @@ -27,7 +29,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * Class for a collection of dictionaries that behave like one dictionary. */ public class DictionaryCollection extends Dictionary { - + private final String TAG = DictionaryCollection.class.getSimpleName(); protected final List<Dictionary> mDictionaries; public DictionaryCollection() { @@ -75,7 +77,21 @@ public class DictionaryCollection extends Dictionary { dict.close(); } - public void addDictionary(Dictionary newDict) { - if (null != newDict) mDictionaries.add(newDict); + // Warning: this is not thread-safe. Take necessary precaution when calling. + public void addDictionary(final Dictionary newDict) { + if (null == newDict) return; + if (mDictionaries.contains(newDict)) { + Log.w(TAG, "This collection already contains this dictionary: " + newDict); + } + mDictionaries.add(newDict); + } + + // Warning: this is not thread-safe. Take necessary precaution when calling. + public void removeDictionary(final Dictionary dict) { + if (mDictionaries.contains(dict)) { + mDictionaries.remove(dict); + } else { + Log.w(TAG, "This collection does not contain this dictionary: " + dict); + } } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index cad69bb0e..7eec8e2ca 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -51,6 +51,7 @@ public class ExpandableDictionary extends Dictionary { private Object mUpdatingLock = new Object(); private static class Node { + Node() {} char mCode; int mFrequency; boolean mTerminal; @@ -547,6 +548,7 @@ public class ExpandableDictionary extends Dictionary { } private class LoadDictionaryTask extends Thread { + LoadDictionaryTask() {} @Override public void run() { loadDictionaryAsync(); diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java new file mode 100644 index 000000000..f5cf953c4 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 android.text.InputType; +import android.util.Log; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.compat.InputTypeCompatUtils; + +/** + * Class to hold attributes of the input field. + */ +public class InputAttributes { + private final String TAG = InputAttributes.class.getSimpleName(); + + final public boolean mInsertSpaceOnPickSuggestionManually; + final public boolean mInputTypeNoAutoCorrect; + final public boolean mIsSettingsSuggestionStripOn; + final public boolean mApplicationSpecifiedCompletionOn; + + public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) { + final int inputType = null != editorInfo ? editorInfo.inputType : 0; + final int inputClass = inputType & InputType.TYPE_MASK_CLASS; + if (inputClass != InputType.TYPE_CLASS_TEXT) { + // If we are not looking at a TYPE_CLASS_TEXT field, the following strange + // cases may arise, so we do a couple sanity checks for them. If it's a + // TYPE_CLASS_TEXT field, these special cases cannot happen, by construction + // of the flags. + if (null == editorInfo) { + Log.w(TAG, "No editor info for this field. Bug?"); + } else if (InputType.TYPE_NULL == inputType) { + // TODO: We should honor TYPE_NULL specification. + Log.i(TAG, "InputType.TYPE_NULL is specified"); + } else if (inputClass == 0) { + // TODO: is this check still necessary? + Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x" + + " imeOptions=0x%08x", + inputType, editorInfo.imeOptions)); + } + mInsertSpaceOnPickSuggestionManually = false; + mIsSettingsSuggestionStripOn = false; + mInputTypeNoAutoCorrect = false; + mApplicationSpecifiedCompletionOn = false; + } else { + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + final boolean flagNoSuggestions = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + final boolean flagMultiLine = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE); + final boolean flagAutoCorrect = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + final boolean flagAutoComplete = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); + + // Make sure that passwords are not displayed in {@link SuggestionsView}. + if (InputTypeCompatUtils.isPasswordInputType(inputType) + || InputTypeCompatUtils.isVisiblePasswordInputType(inputType) + || InputTypeCompatUtils.isEmailVariation(variation) + || InputType.TYPE_TEXT_VARIATION_URI == variation + || InputType.TYPE_TEXT_VARIATION_FILTER == variation + || flagNoSuggestions + || flagAutoComplete) { + mIsSettingsSuggestionStripOn = false; + } else { + mIsSettingsSuggestionStripOn = true; + } + + if (InputTypeCompatUtils.isEmailVariation(variation) + || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { + // The point in turning this off is that we don't want to insert a space after + // a name when filling a form: we can't delete trailing spaces when changing fields + mInsertSpaceOnPickSuggestionManually = false; + } else { + mInsertSpaceOnPickSuggestionManually = true; + } + + // If it's a browser edit field and auto correct is not ON explicitly, then + // disable auto correction, but keep suggestions on. + // If NO_SUGGESTIONS is set, don't do prediction. + // If it's not multiline and the autoCorrect flag is not set, then don't correct + if ((variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT + && !flagAutoCorrect) + || flagNoSuggestions + || (!flagAutoCorrect && !flagMultiLine)) { + mInputTypeNoAutoCorrect = true; + } else { + mInputTypeNoAutoCorrect = false; + } + + mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; + } + } + + // Pretty print + @Override + public String toString() { + return "\n mInsertSpaceOnPickSuggestionManually = " + mInsertSpaceOnPickSuggestionManually + + "\n mInputTypeNoAutoCorrect = " + mInputTypeNoAutoCorrect + + "\n mIsSettingsSuggestionStripOn = " + mIsSettingsSuggestionStripOn + + "\n mApplicationSpecifiedCompletionOn = " + mApplicationSpecifiedCompletionOn; + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 9c321bcb9..59de798d8 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -62,10 +62,11 @@ import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; +import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; -import com.android.inputmethod.keyboard.LatinKeyboard; import com.android.inputmethod.keyboard.LatinKeyboardView; +import com.android.inputmethod.latin.suggestions.SuggestionsView; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -77,7 +78,6 @@ import java.util.Locale; public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener, SuggestionsView.Listener { private static final String TAG = LatinIME.class.getSimpleName(); - private static final boolean PERF_DEBUG = false; private static final boolean TRACE = false; private static boolean DEBUG; @@ -143,6 +143,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar */ private static final String SCHEME_PACKAGE = "package"; + // TODO: migrate this to SettingsValues private int mSuggestionVisibility; private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE = R.string.prefs_suggestion_visibility_show_value; @@ -157,7 +158,24 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar SUGGESTION_VISIBILILTY_HIDE_VALUE }; - private Settings.Values mSettingsValues; + // Magic space: a space that should disappear on space/apostrophe insertion, move after the + // punctuation on punctuation insertion, and become a real space on alpha char insertion. + // Weak space: a space that should be swapped only by suggestion strip punctuation. + // Double space: the state where the user pressed space twice quickly, which LatinIME + // resolved as period-space. Undoing this converts the period to a space. + // Swap punctuation: the state where a (weak or magic) space and a punctuation from the + // suggestion strip have just been swapped. Undoing this swaps them back. + private static final int SPACE_STATE_NONE = 0; + private static final int SPACE_STATE_DOUBLE = 1; + private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; + private static final int SPACE_STATE_MAGIC = 3; + private static final int SPACE_STATE_WEAK = 4; + + // Current space state of the input method. This can be any of the above constants. + private int mSpaceState; + + private SettingsValues mSettingsValues; + private InputAttributes mInputAttributes; private View mExtractArea; private View mKeyPreviewBackingView; @@ -169,7 +187,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private InputMethodManagerCompatWrapper mImm; private Resources mResources; private SharedPreferences mPrefs; - private String mInputMethodId; private KeyboardSwitcher mKeyboardSwitcher; private SubtypeSwitcher mSubtypeSwitcher; private VoiceProxy mVoiceProxy; @@ -177,28 +194,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private UserUnigramDictionary mUserUnigramDictionary; - private boolean mIsUserDictionaryAvaliable; - - // TODO: Create an inner class to group options and pseudo-options to improve readability. - // These variables are initialized according to the {@link EditorInfo#inputType}. - private boolean mInsertSpaceOnPickSuggestionManually; - private boolean mInputTypeNoAutoCorrect; - private boolean mIsSettingsSuggestionStripOn; - private boolean mApplicationSpecifiedCompletionOn; + private boolean mIsUserDictionaryAvailable; - private final StringBuilder mComposingStringBuilder = new StringBuilder(); private WordComposer mWordComposer = new WordComposer(); - private CharSequence mBestWord; - private boolean mHasUncommittedTypedChars; - // Magic space: a space that should disappear on space/apostrophe insertion, move after the - // punctuation on punctuation insertion, and become a real space on alpha char insertion. - private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space. - // This indicates whether the last keypress resulted in processing of double space replacement - // with period-space. - private boolean mJustReplacedDoubleSpace; private int mCorrectionMode; - private int mCommittedLength; // Keep track of the last selection range to decide if we need to show word alternatives private int mLastSelectionStart; private int mLastSelectionEnd; @@ -210,11 +210,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private long mLastKeyTime; private AudioManager mAudioManager; - private float mFxVolume = -1.0f; // default volume private boolean mSilentModeOn; // System-wide current configuration private VibratorCompatWrapper mVibrator; - private long mKeypressVibrationDuration = -1; // TODO: Move this flag to VoiceProxy private boolean mConfigurationChanging; @@ -241,9 +239,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3; private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4; private static final int MSG_SPACE_TYPED = 5; - private static final int MSG_KEY_TYPED = 6; - private static final int MSG_SET_BIGRAM_PREDICTIONS = 7; - private static final int MSG_PENDING_IMS_CALLBACK = 8; + private static final int MSG_SET_BIGRAM_PREDICTIONS = 6; + private static final int MSG_PENDING_IMS_CALLBACK = 7; private int mDelayBeforeFadeoutLanguageOnSpacebar; private int mDelayUpdateSuggestions; @@ -251,7 +248,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private int mDurationOfFadeoutLanguageOnSpacebar; private float mFinalFadeoutFactorOfLanguageOnSpacebar; private long mDoubleSpacesTurnIntoPeriodTimeout; - private long mIgnoreSpecialKeyTimeout; public UIHandler(LatinIME outerInstance) { super(outerInstance); @@ -271,8 +267,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( R.integer.config_double_spaces_turn_into_period_timeout); - mIgnoreSpecialKeyTimeout = res.getInteger( - R.integer.config_ignore_special_key_timeout); } @Override @@ -295,19 +289,15 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked())); break; case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: - if (inputView != null) { - inputView.setSpacebarTextFadeFactor( - (1.0f + mFinalFadeoutFactorOfLanguageOnSpacebar) / 2, - (LatinKeyboard)msg.obj); - } + setSpacebarTextFadeFactor(inputView, + (1.0f + mFinalFadeoutFactorOfLanguageOnSpacebar) / 2, + (Keyboard)msg.obj); sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), mDurationOfFadeoutLanguageOnSpacebar); break; case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: - if (inputView != null) { - inputView.setSpacebarTextFadeFactor(mFinalFadeoutFactorOfLanguageOnSpacebar, - (LatinKeyboard)msg.obj); - } + setSpacebarTextFadeFactor(inputView, mFinalFadeoutFactorOfLanguageOnSpacebar, + (Keyboard)msg.obj); break; } } @@ -347,21 +337,32 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar sendMessage(obtainMessage(MSG_VOICE_RESULTS)); } + private static void setSpacebarTextFadeFactor(LatinKeyboardView inputView, + float fadeFactor, Keyboard oldKeyboard) { + if (inputView == null) return; + final Keyboard keyboard = inputView.getKeyboard(); + if (keyboard == oldKeyboard) { + inputView.updateSpacebar(fadeFactor, + SubtypeSwitcher.getInstance().needsToDisplayLanguage( + keyboard.mId.mLocale)); + } + } + public void startDisplayLanguageOnSpacebar(boolean localeChanged) { final LatinIME latinIme = getOuterInstance(); removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView(); if (inputView != null) { - final LatinKeyboard keyboard = latinIme.mKeyboardSwitcher.getLatinKeyboard(); + final Keyboard keyboard = latinIme.mKeyboardSwitcher.getKeyboard(); // The language is always displayed when the delay is negative. final boolean needsToDisplayLanguage = localeChanged || mDelayBeforeFadeoutLanguageOnSpacebar < 0; // The language is never displayed when the delay is zero. if (mDelayBeforeFadeoutLanguageOnSpacebar != 0) { - inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f - : mFinalFadeoutFactorOfLanguageOnSpacebar, - keyboard); + setSpacebarTextFadeFactor(inputView, + needsToDisplayLanguage ? 1.0f : mFinalFadeoutFactorOfLanguageOnSpacebar, + keyboard); } // The fadeout animation will start when the delay is positive. if (localeChanged && mDelayBeforeFadeoutLanguageOnSpacebar > 0) { @@ -384,21 +385,13 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return hasMessages(MSG_SPACE_TYPED); } - public void startKeyTypedTimer() { - removeMessages(MSG_KEY_TYPED); - sendMessageDelayed(obtainMessage(MSG_KEY_TYPED), mIgnoreSpecialKeyTimeout); - } - - public boolean isIgnoringSpecialKey() { - return hasMessages(MSG_KEY_TYPED); - } - // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccesiveImsCallback; private boolean mHasPendingStartInput; private boolean mHasPendingFinishInputView; private boolean mHasPendingFinishInput; + private EditorInfo mAppliedEditorInfo; public void startOrientationChanging() { removeMessages(MSG_PENDING_IMS_CALLBACK); @@ -416,18 +409,18 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHasPendingStartInput = false; } - private void executePendingImsCallback(LatinIME latinIme, EditorInfo attribute, + private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) latinIme.onFinishInputViewInternal(mHasPendingFinishInput); if (mHasPendingFinishInput) latinIme.onFinishInputInternal(); if (mHasPendingStartInput) - latinIme.onStartInputInternal(attribute, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); resetPendingImsCallback(); } - public void onStartInput(EditorInfo attribute, boolean restarting) { + public void onStartInput(EditorInfo editorInfo, boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; @@ -438,27 +431,29 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mPendingSuccesiveImsCallback = true; } final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, attribute, restarting); - latinIme.onStartInputInternal(attribute, restarting); + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); } } - public void onStartInputView(EditorInfo attribute, boolean restarting) { - if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { - // Typically this is the second onStartInputView after orientation changed. - resetPendingImsCallback(); - } else { - if (mPendingSuccesiveImsCallback) { - // This is the first onStartInputView after orientation changed. - mPendingSuccesiveImsCallback = false; - resetPendingImsCallback(); - sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), - PENDING_IMS_CALLBACK_DURATION); - } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, attribute, restarting); - latinIme.onStartInputViewInternal(attribute, restarting); - } + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + if (hasMessages(MSG_PENDING_IMS_CALLBACK) + && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { + // Typically this is the second onStartInputView after orientation changed. + resetPendingImsCallback(); + } else { + if (mPendingSuccesiveImsCallback) { + // This is the first onStartInputView after orientation changed. + mPendingSuccesiveImsCallback = false; + resetPendingImsCallback(); + sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), + PENDING_IMS_CALLBACK_DURATION); + } + final LatinIME latinIme = getOuterInstance(); + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputViewInternal(editorInfo, restarting); + mAppliedEditorInfo = editorInfo; + } } public void onFinishInputView(boolean finishingInput) { @@ -468,6 +463,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } else { final LatinIME latinIme = getOuterInstance(); latinIme.onFinishInputViewInternal(finishingInput); + mAppliedEditorInfo = null; } } @@ -492,12 +488,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar InputMethodManagerCompatWrapper.init(this); SubtypeSwitcher.init(this); KeyboardSwitcher.init(this, prefs); - AccessibilityUtils.init(this, prefs); + AccessibilityUtils.init(this); super.onCreate(); mImm = InputMethodManagerCompatWrapper.getInstance(); - mInputMethodId = Utils.getInputMethodId(mImm, getPackageName()); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mVibrator = VibratorCompatWrapper.getInstance(this); @@ -509,6 +504,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar loadSettings(); + // TODO: remove the following when it's not needed by updateCorrectionMode() any more + mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); Utils.GCUtils.getInstance().reset(); boolean tryGC = true; for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { @@ -546,10 +543,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar /* package */ void loadSettings() { if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this); if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance(); - mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr()); + mSettingsValues = new SettingsValues(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr()); resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); - updateSoundEffectVolume(); - updateKeypressVibrationDuration(); } private void initSuggest() { @@ -574,7 +569,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mUserDictionary = new UserDictionary(this, localeStr); mSuggest.setUserDictionary(mUserDictionary); - mIsUserDictionaryAvaliable = mUserDictionary.isEnabled(); + mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); resetContactsDictionary(oldContactsDictionary); @@ -695,13 +690,13 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } @Override - public void onStartInput(EditorInfo attribute, boolean restarting) { - mHandler.onStartInput(attribute, restarting); + public void onStartInput(EditorInfo editorInfo, boolean restarting) { + mHandler.onStartInput(editorInfo, restarting); } @Override - public void onStartInputView(EditorInfo attribute, boolean restarting) { - mHandler.onStartInputView(attribute, restarting); + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + mHandler.onStartInputView(editorInfo, restarting); } @Override @@ -714,20 +709,21 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHandler.onFinishInput(); } - private void onStartInputInternal(EditorInfo attribute, boolean restarting) { - super.onStartInput(attribute, restarting); + private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) { + super.onStartInput(editorInfo, restarting); } - private void onStartInputViewInternal(EditorInfo attribute, boolean restarting) { - super.onStartInputView(attribute, restarting); + private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { + super.onStartInputView(editorInfo, restarting); final KeyboardSwitcher switcher = mKeyboardSwitcher; LatinKeyboardView inputView = switcher.getKeyboardView(); if (DEBUG) { - Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none" + Log.d(TAG, "onStartInputView: editorInfo:" + ((editorInfo == null) ? "none" : String.format("inputType=0x%08x imeOptions=0x%08x", - attribute.inputType, attribute.imeOptions))); + editorInfo.inputType, editorInfo.imeOptions))); } + LatinImeLogger.onStartInputView(editorInfo); // In landscape mode, this method gets called without the input view being created. if (inputView == null) { return; @@ -736,47 +732,44 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { - accessUtils.onStartInputViewInternal(attribute, restarting); + accessUtils.onStartInputViewInternal(editorInfo, restarting); } mSubtypeSwitcher.updateParametersOnStartInputView(); - TextEntryState.reset(); - // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to // know now whether this is a password text field, because we need to know now whether we // want to enable the voice button. final VoiceProxy voiceIme = mVoiceProxy; - final int inputType = (attribute != null) ? attribute.inputType : 0; + final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType) || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)); // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); - initializeInputAttributes(attribute); + mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); + mApplicationSpecifiedCompletions = null; inputView.closing(); mEnteredText = null; - mComposingStringBuilder.setLength(0); - mHasUncommittedTypedChars = false; + mWordComposer.reset(); mDeleteCount = 0; - mJustAddedMagicSpace = false; - mJustReplacedDoubleSpace = false; + mSpaceState = SPACE_STATE_NONE; loadSettings(); updateCorrectionMode(); - updateSuggestionVisibility(mPrefs, mResources); + updateSuggestionVisibility(mResources); if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); } - mVoiceProxy.loadSettings(attribute, mPrefs); + mVoiceProxy.loadSettings(editorInfo, mPrefs); // This will work only when the subtype is not supported. LanguageSwitcherProxy.loadSettings(); if (mSubtypeSwitcher.isKeyboardMode()) { - switcher.loadKeyboard(attribute, mSettingsValues); + switcher.loadKeyboard(editorInfo, mSettingsValues); } if (mSuggestionsView != null) @@ -785,6 +778,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar isSuggestionsStripVisible(), /* needsInputViewShown */ false); // Delay updating suggestions because keyboard input view may not be shown at this point. mHandler.postUpdateSuggestions(); + mHandler.cancelDoubleSpacesTimer(); inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, mSettingsValues.mKeyPreviewPopupDismissDelay); @@ -795,73 +789,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } - private void initializeInputAttributes(EditorInfo attribute) { - if (attribute == null) - return; - final int inputType = attribute.inputType; - if (inputType == InputType.TYPE_NULL) { - // TODO: We should honor TYPE_NULL specification. - Log.i(TAG, "InputType.TYPE_NULL is specified"); - } - final int inputClass = inputType & InputType.TYPE_MASK_CLASS; - final int variation = inputType & InputType.TYPE_MASK_VARIATION; - if (inputClass == 0) { - Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x imeOptions=0x%08x", - inputType, attribute.imeOptions)); - } - - mInsertSpaceOnPickSuggestionManually = false; - mInputTypeNoAutoCorrect = false; - mIsSettingsSuggestionStripOn = false; - mApplicationSpecifiedCompletionOn = false; - mApplicationSpecifiedCompletions = null; - - if (inputClass == InputType.TYPE_CLASS_TEXT) { - mIsSettingsSuggestionStripOn = true; - // Make sure that passwords are not displayed in {@link SuggestionsView}. - if (InputTypeCompatUtils.isPasswordInputType(inputType) - || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) { - mIsSettingsSuggestionStripOn = false; - } - if (InputTypeCompatUtils.isEmailVariation(variation) - || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { - // The point in turning this off is that we don't want to insert a space after - // a name when filling a form: we can't delete trailing spaces when changing fields - mInsertSpaceOnPickSuggestionManually = false; - } else { - mInsertSpaceOnPickSuggestionManually = true; - } - if (InputTypeCompatUtils.isEmailVariation(variation)) { - mIsSettingsSuggestionStripOn = false; - } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { - mIsSettingsSuggestionStripOn = false; - } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - mIsSettingsSuggestionStripOn = false; - } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { - // If it's a browser edit field and auto correct is not ON explicitly, then - // disable auto correction, but keep suggestions on. - if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { - mInputTypeNoAutoCorrect = true; - } - } - - // If NO_SUGGESTIONS is set, don't do prediction. - if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { - mIsSettingsSuggestionStripOn = false; - mInputTypeNoAutoCorrect = true; - } - // If it's not multiline and the autoCorrect flag is not set, then don't correct - if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 - && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { - mInputTypeNoAutoCorrect = true; - } - if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { - mIsSettingsSuggestionStripOn = false; - mApplicationSpecifiedCompletionOn = isFullscreenMode(); - } - } - } - @Override public void onWindowHidden() { super.onWindowHidden(); @@ -923,12 +850,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart; final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1; if (!mExpectingUpdateSelection) { - if (((mComposingStringBuilder.length() > 0 && mHasUncommittedTypedChars) + // TAKE CARE: there is a race condition when we enter this test even when the user + // did not explicitly move the cursor. This happens when typing fast, where two keys + // turn this flag on in succession and both onUpdateSelection() calls arrive after + // the second one - the first call successfully avoids this test, but the second one + // enters. For the moment we rely on candidatesCleared to further reduce the impact. + if (SPACE_STATE_WEAK == mSpaceState) { + // Test for no WEAK_SPACE action because there is a race condition that may end up + // in coming here on a normal key press. We set this to NONE because after + // a cursor move, we don't want the suggestion strip to swap the space with the + // newly inserted punctuation. + mSpaceState = SPACE_STATE_NONE; + } + if (((mWordComposer.isComposingWord()) || mVoiceProxy.isVoiceInputHighlighted()) && (selectionChanged || candidatesCleared)) { - mComposingStringBuilder.setLength(0); - mHasUncommittedTypedChars = false; - TextEntryState.reset(); + mWordComposer.reset(); updateSuggestions(); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { @@ -936,26 +873,23 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } mComposingStateManager.onFinishComposingText(); mVoiceProxy.setVoiceInputHighlighted(false); - } else if (!mHasUncommittedTypedChars) { - TextEntryState.reset(); + } else if (!mWordComposer.isComposingWord()) { + mWordComposer.reset(); updateSuggestions(); } - mJustAddedMagicSpace = false; // The user moved the cursor. - mJustReplacedDoubleSpace = false; } mExpectingUpdateSelection = false; mHandler.postUpdateShiftKeyState(); + // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not + // here. It would probably be too expensive to call directly here but we may want to post a + // message to delay it. The point would be to unify behavior between backspace to the + // end of a word and manually put the pointer at the end of the word. // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; } - public void setLastSelection(int start, int end) { - mLastSelectionStart = start; - mLastSelectionEnd = end; - } - /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides @@ -1011,7 +945,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } } - if (mApplicationSpecifiedCompletionOn) { + if (mInputAttributes.mApplicationSpecifiedCompletionOn) { mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestions(); @@ -1024,7 +958,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar .setHasMinimalSuggestion(false); // When in fullscreen mode, show completions generated by the application setSuggestions(builder.build()); - mBestWord = null; + mWordComposer.deleteAutoCorrection(); setSuggestionStripShown(true); } } @@ -1085,8 +1019,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar @Override public boolean onEvaluateFullscreenMode() { - return super.onEvaluateFullscreenMode() - && mResources.getBoolean(R.bool.config_use_fullscreen_mode); + return super.onEvaluateFullscreenMode() && mSettingsValues.mUseFullScreenMode; } @Override @@ -1142,15 +1075,14 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } public void commitTyped(final InputConnection ic) { - if (!mHasUncommittedTypedChars) return; - mHasUncommittedTypedChars = false; - if (mComposingStringBuilder.length() > 0) { + if (!mWordComposer.isComposingWord()) return; + final CharSequence typedWord = mWordComposer.getTypedWord(); + mWordComposer.onCommitWord(WordComposer.COMMIT_TYPE_USER_TYPED_WORD); + if (typedWord.length() > 0) { if (ic != null) { - ic.commitText(mComposingStringBuilder, 1); + ic.commitText(typedWord, 1); } - mCommittedLength = mComposingStringBuilder.length(); - TextEntryState.acceptedTyped(mComposingStringBuilder); - addToUserUnigramAndBigramDictionaries(mComposingStringBuilder, + addToUserUnigramAndBigramDictionaries(typedWord, UserUnigramDictionary.FREQUENCY_FOR_TYPED); } updateSuggestions(); @@ -1166,25 +1098,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return false; } - private void swapSwapperAndSpace() { - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; + // "ic" may be null + private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) { + if (null == ic) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { - ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); - ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); } } - private void maybeDoubleSpace() { - if (mCorrectionMode == Suggest.CORRECTION_NONE) return; - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; + private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) { + if (mCorrectionMode == Suggest.CORRECTION_NONE) return false; + if (ic == null) return false; final CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && Utils.canBeFollowedByPeriod(lastThree.charAt(0)) @@ -1192,22 +1121,19 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar && lastThree.charAt(2) == Keyboard.CODE_SPACE && mHandler.isAcceptingDoubleSpaces()) { mHandler.cancelDoubleSpacesTimer(); - ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); - ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); - mJustReplacedDoubleSpace = true; - } else { - mHandler.startDoubleSpacesTimer(); + return true; } + return false; } - // "ic" must not null - private void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) { + // "ic" must not be null + private static void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) { // When the text's first character is '.', remove the previous period // if there is one. - CharSequence lastOne = ic.getTextBeforeCursor(1, 0); + final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_PERIOD && text.charAt(0) == Keyboard.CODE_PERIOD) { @@ -1215,11 +1141,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } - private void removeTrailingSpace() { - final InputConnection ic = getCurrentInputConnection(); + // "ic" may be null + private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) { if (ic == null) return; - - CharSequence lastOne = ic.getTextBeforeCursor(1, 0); + final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { ic.deleteSurroundingText(1, 0); @@ -1235,19 +1160,15 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return true; } - private boolean isAlphabet(int code) { - if (Character.isLetter(code)) { - return true; - } else { - return false; - } + private static boolean isAlphabet(int code) { + return Character.isLetter(code); } private void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { showSubtypeSelectorAndSettings(); - } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, false /* exclude aux subtypes */)) { + } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(false /* exclude aux subtypes */)) { showOptionsMenu(); } else { launchSettings(); @@ -1256,17 +1177,21 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // Virtual codes representing custom requests. These are used in onCustomRequest() below. public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; + public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2; @Override public boolean onCustomRequest(int requestCode) { if (isShowingOptionDialog()) return false; switch (requestCode) { case CODE_SHOW_INPUT_METHOD_PICKER: - if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm, true /* include aux subtypes */)) { + if (Utils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { mImm.showInputMethodPicker(); return true; } return false; + case CODE_HAPTIC_AND_AUDIO_FEEDBACK: + hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED); + return true; } return false; } @@ -1275,6 +1200,29 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return mOptionsDialog != null && mOptionsDialog.isShowing(); } + private void insertPunctuationFromSuggestionStrip(final InputConnection ic, final int code) { + final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : null; + final int toLeft = TextUtils.isEmpty(beforeText) ? 0 : beforeText.charAt(0); + final boolean shouldRegisterSwapPunctuation; + // If we have a space left of the cursor and it's a weak or a magic space, then we should + // swap it, and override the space state with SPACESTATE_SWAP_PUNCTUATION. + // To swap it, we fool handleSeparator to think the previous space state was a + // magic space. + if (Keyboard.CODE_SPACE == toLeft && mSpaceState == SPACE_STATE_WEAK + && mSettingsValues.isMagicSpaceSwapper(code)) { + mSpaceState = SPACE_STATE_MAGIC; + shouldRegisterSwapPunctuation = true; + } else { + shouldRegisterSwapPunctuation = false; + } + onCodeInput(code, new int[] { code }, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE); + if (shouldRegisterSwapPunctuation) { + mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; + } + } + // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { @@ -1285,12 +1233,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mLastKeyTime = when; final KeyboardSwitcher switcher = mKeyboardSwitcher; final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); - final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace; - mJustReplacedDoubleSpace = false; - boolean shouldStartKeyTypedTimer = true; + // The space state depends only on the last character pressed and its own previous + // state. Here, we revert the space state to neutral if the key is actually modifying + // the input contents (any non-shift key), which is what we should do for + // all inputs that do not result in a special state. Each character handling is then + // free to override the state as they see fit. + final int spaceState = mSpaceState; + + // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. + if (primaryCode != Keyboard.CODE_SPACE) { + mHandler.cancelDoubleSpacesTimer(); + } + switch (primaryCode) { case Keyboard.CODE_DELETE: - handleBackspace(lastStateOfJustReplacedDoubleSpace); + mSpaceState = SPACE_STATE_NONE; + handleBackspace(spaceState); mDeleteCount++; mExpectingUpdateSelection = true; LatinImeLogger.logOnDelete(); @@ -1300,39 +1258,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (!distinctMultiTouch) { switcher.toggleShift(); } - shouldStartKeyTypedTimer = false; break; case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: // Symbol key is handled in onPress() when device has distinct multi-touch panel. if (!distinctMultiTouch) { - switcher.changeKeyboardMode(); - } - shouldStartKeyTypedTimer = false; - break; - case Keyboard.CODE_CANCEL: - if (!isShowingOptionDialog()) { - handleClose(); + switcher.toggleAlphabetAndSymbols(); } break; case Keyboard.CODE_SETTINGS: - if (!mHandler.isIgnoringSpecialKey()) { - onSettingsKeyPressed(); - } - shouldStartKeyTypedTimer = false; + onSettingsKeyPressed(); break; case Keyboard.CODE_CAPSLOCK: switcher.toggleCapsLock(); - //$FALL-THROUGH$ - case Keyboard.CODE_HAPTIC_AND_AUDIO_FEEDBACK_ONLY: - // Dummy code for haptic and audio feedbacks. - vibrate(); - playKeyClick(primaryCode); + hapticAndAudioFeedback(primaryCode); break; case Keyboard.CODE_SHORTCUT: - if (!mHandler.isIgnoringSpecialKey()) { - mSubtypeSwitcher.switchToShortcutIME(); - } - shouldStartKeyTypedTimer = false; + mSubtypeSwitcher.switchToShortcutIME(); break; case Keyboard.CODE_TAB: handleTab(); @@ -1346,20 +1287,18 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // To sum it up: do not update mExpectingUpdateSelection here. break; default: + mSpaceState = SPACE_STATE_NONE; if (mSettingsValues.isWordSeparator(primaryCode)) { - handleSeparator(primaryCode, x, y); + handleSeparator(primaryCode, x, y, spaceState); } else { - handleCharacter(primaryCode, keyCodes, x, y); + handleCharacter(primaryCode, keyCodes, x, y, spaceState); } mExpectingUpdateSelection = true; break; } - switcher.onKey(primaryCode); + switcher.onCodeInput(primaryCode); // Reset after any single keystroke mEnteredText = null; - if (shouldStartKeyTypedTimer) { - mHandler.startKeyTypedTimer(); - } } @Override @@ -1373,10 +1312,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar ic.commitText(text, 1); ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); - mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); - mJustAddedMagicSpace = false; + mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); + mSpaceState = SPACE_STATE_NONE; mEnteredText = text; - mHandler.startKeyTypedTimer(); + mWordComposer.reset(); } @Override @@ -1385,60 +1324,70 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mKeyboardSwitcher.onCancelInput(); } - private void handleBackspace(boolean justReplacedDoubleSpace) { + private void handleBackspace(final int spaceState) { if (mVoiceProxy.logAndRevertVoiceInput()) return; - final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); + handleBackspaceWhileInBatchEdit(spaceState, ic); + ic.endBatchEdit(); + } + // "ic" may not be null. + private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) { mVoiceProxy.handleBackspace(); - final boolean deleteChar = !mHasUncommittedTypedChars; - if (mHasUncommittedTypedChars) { - final int length = mComposingStringBuilder.length(); + // In many cases, we may have to put the keyboard in auto-shift state again. + mHandler.postUpdateShiftKeyState(); + + if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { + // Cancel multi-character input: remove the text we just entered. + // This is triggered on backspace after a key that inputs multiple characters, + // like the smiley key or the .com key. + ic.deleteSurroundingText(mEnteredText.length(), 0); + // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. + // In addition we know that spaceState is false, and that we should not be + // reverting any autocorrect at this point. So we can safely return. + return; + } + + if (mWordComposer.isComposingWord()) { + final int length = mWordComposer.size(); if (length > 0) { - mComposingStringBuilder.delete(length - 1, length); mWordComposer.deleteLast(); - final CharSequence textWithUnderline = - mComposingStateManager.isAutoCorrectionIndicatorOn() - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( - this, mComposingStringBuilder) - : mComposingStringBuilder; - ic.setComposingText(textWithUnderline, 1); - if (mComposingStringBuilder.length() == 0) { - mHasUncommittedTypedChars = false; - } - if (1 == length) { - // 1 == length means we are about to erase the last character of the word, - // so we can show bigrams. + ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + // If we have deleted the last remaining character of a word, then we are not + // isComposingWord() any more. + if (!mWordComposer.isComposingWord()) { + // Not composing word any more, so we can show bigrams. mHandler.postUpdateBigramPredictions(); } else { - // length > 1, so we still have letters to deduce a suggestion from. + // Still composing a word, so we still have letters to deduce a suggestion from. mHandler.postUpdateSuggestions(); } } else { ic.deleteSurroundingText(1, 0); } - } - mHandler.postUpdateShiftKeyState(); - - TextEntryState.backspace(); - if (TextEntryState.isUndoCommit()) { - revertLastWord(ic); - ic.endBatchEdit(); - return; - } - if (justReplacedDoubleSpace) { - if (revertDoubleSpace(ic)) { - ic.endBatchEdit(); + } else { + if (mWordComposer.didAutoCorrectToAnotherWord()) { + Utils.Stats.onAutoCorrectionCancellation(); + cancelAutoCorrect(ic); return; } - } - if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { - ic.deleteSurroundingText(mEnteredText.length(), 0); - } else if (deleteChar) { + if (SPACE_STATE_DOUBLE == spaceState) { + if (revertDoubleSpace(ic)) { + // No need to reset mSpaceState, it has already be done (that's why we + // receive it as a parameter) + return; + } + } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { + if (revertSwapPunctuation(ic)) { + // Likewise + return; + } + } + if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) { // Go back to the suggestion mode if the user canceled the // "Touch again to save". @@ -1447,15 +1396,15 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // different behavior only in the case of picking the first // suggestion (typed word). It's intentional to have made this // inconsistent with backspacing after selecting other suggestions. - revertLastWord(ic); + restartSuggestionsOnManuallyPickedTypedWord(ic); } else { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + ic.deleteSurroundingText(1, 0); if (mDeleteCount > DELETE_ACCELERATE_AT) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + ic.deleteSurroundingText(1, 0); } + restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); } } - ic.endBatchEdit(); } private void handleTab() { @@ -1481,19 +1430,34 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } - private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) { + private void handleCharacter(final int primaryCode, final int[] keyCodes, final int x, + final int y, final int spaceState) { mVoiceProxy.handleCharacter(); + final InputConnection ic = getCurrentInputConnection(); + if (null != ic) ic.beginBatchEdit(); + // TODO: if ic is null, does it make any sense to call this? + handleCharacterWhileInBatchEdit(primaryCode, keyCodes, x, y, spaceState, ic); + if (null != ic) ic.endBatchEdit(); + } - if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) { - removeTrailingSpace(); + // "ic" may be null without this crashing, but the behavior will be really strange + private void handleCharacterWhileInBatchEdit(final int primaryCode, final int[] keyCodes, + final int x, final int y, final int spaceState, final InputConnection ic) { + if (SPACE_STATE_MAGIC == spaceState + && mSettingsValues.isMagicSpaceStripper(primaryCode)) { + if (null != ic) removeTrailingSpaceWhileInBatchEdit(ic); } + boolean isComposingWord = mWordComposer.isComposingWord(); int code = primaryCode; if ((isAlphabet(code) || mSettingsValues.isSymbolExcludedFromWordSeparators(code)) && isSuggestionsRequested() && !isCursorTouchingWord()) { - if (!mHasUncommittedTypedChars) { - mHasUncommittedTypedChars = true; - mComposingStringBuilder.setLength(0); + if (!isComposingWord) { + // Reset entirely the composing state anyway, then start composing a new word unless + // the character is a single quote. The idea here is, single quote is not a + // separator and it should be treated as a normal character, except in the first + // position where it should not start composing a word. + isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != code); mWordComposer.reset(); clearSuggestions(); mComposingStateManager.onFinishComposingText(); @@ -1520,39 +1484,34 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } } - if (mHasUncommittedTypedChars) { - mComposingStringBuilder.append((char) code); + if (isComposingWord) { mWordComposer.add(code, keyCodes, x, y); - final InputConnection ic = getCurrentInputConnection(); if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWordComposer.size() == 1) { mWordComposer.setAutoCapitalized(getCurrentAutoCapsState()); mComposingStateManager.onStartComposingText(); } - final CharSequence textWithUnderline = - mComposingStateManager.isAutoCorrectionIndicatorOn() - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( - this, mComposingStringBuilder) - : mComposingStringBuilder; - ic.setComposingText(textWithUnderline, 1); + ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } mHandler.postUpdateSuggestions(); } else { sendKeyChar((char)code); } - if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { - swapSwapperAndSpace(); - } else { - mJustAddedMagicSpace = false; + if (SPACE_STATE_MAGIC == spaceState + && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { + if (null != ic) swapSwapperAndSpaceWhileInBatchEdit(ic); } - switcher.updateShiftState(); - if (LatinIME.PERF_DEBUG) measureCps(); - TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y); + if (mSettingsValues.isWordSeparator(code)) { + Utils.Stats.onSeparator((char)code, x, y); + } else { + Utils.Stats.onNonSeparator((char)code, x, y); + } } - private void handleSeparator(int primaryCode, int x, int y) { + private void handleSeparator(final int primaryCode, final int x, final int y, + final int spaceState) { mVoiceProxy.handleSeparator(); mComposingStateManager.onFinishComposingText(); @@ -1562,69 +1521,83 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHandler.postUpdateSuggestions(); } - boolean pickedDefault = false; // Handle separator final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } - if (mHasUncommittedTypedChars) { + if (mWordComposer.isComposingWord()) { // In certain languages where single quote is a separator, it's better // 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. final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled - && !mInputTypeNoAutoCorrect; + && !mInputAttributes.mInputTypeNoAutoCorrect; if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { - pickedDefault = pickDefaultSuggestion(primaryCode); + commitCurrentAutoCorrection(primaryCode, ic); } else { commitTyped(ic); } } - if (mJustAddedMagicSpace) { + final boolean swapMagicSpace; + if (Keyboard.CODE_ENTER == primaryCode && (SPACE_STATE_MAGIC == spaceState + || SPACE_STATE_SWAP_PUNCTUATION == spaceState)) { + removeTrailingSpaceWhileInBatchEdit(ic); + swapMagicSpace = false; + } else if (SPACE_STATE_MAGIC == spaceState) { if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) { - sendKeyChar((char)primaryCode); - swapSwapperAndSpace(); + swapMagicSpace = true; } else { - if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace(); - sendKeyChar((char)primaryCode); - mJustAddedMagicSpace = false; + swapMagicSpace = false; + if (mSettingsValues.isMagicSpaceStripper(primaryCode)) { + removeTrailingSpaceWhileInBatchEdit(ic); + } } } else { - sendKeyChar((char)primaryCode); + swapMagicSpace = false; } - if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { - maybeDoubleSpace(); - } - - TextEntryState.typedCharacter((char) primaryCode, true, x, y); + sendKeyChar((char)primaryCode); - if (pickedDefault) { - CharSequence typedWord = mWordComposer.getTypedWord(); - TextEntryState.backToAcceptedDefault(typedWord); - if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { - InputConnectionCompatUtils.commitCorrection( - ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); - } - } if (Keyboard.CODE_SPACE == primaryCode) { + if (isSuggestionsRequested()) { + if (maybeDoubleSpaceWhileInBatchEdit(ic)) { + mSpaceState = SPACE_STATE_DOUBLE; + } else if (!isShowingPunctuationList()) { + mSpaceState = SPACE_STATE_WEAK; + } + } + + mHandler.startDoubleSpacesTimer(); if (!isCursorTouchingWord()) { mHandler.cancelUpdateSuggestions(); mHandler.postUpdateBigramPredictions(); } } else { + if (swapMagicSpace) { + swapSwapperAndSpaceWhileInBatchEdit(ic); + mSpaceState = SPACE_STATE_MAGIC; + } + // Set punctuation right away. onUpdateSelection will fire but tests whether it is // already displayed or not, so it's okay. setPunctuationSuggestions(); } - mKeyboardSwitcher.updateShiftState(); + + Utils.Stats.onSeparator((char)primaryCode, x, y); + if (ic != null) { ic.endBatchEdit(); } } + private CharSequence getTextWithUnderline(final CharSequence text) { + return mComposingStateManager.isAutoCorrectionIndicatorOn() + ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) + : mWordComposer.getTypedWord(); + } + private void handleClose() { commitTyped(getCurrentInputConnection()); mVoiceProxy.handleClose(); @@ -1635,7 +1608,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } public boolean isSuggestionsRequested() { - return mIsSettingsSuggestionStripOn + return mInputAttributes.mIsSettingsSuggestionStripOn && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); } @@ -1653,11 +1626,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar public boolean isSuggestionsStripVisible() { if (mSuggestionsView == null) return false; - if (mSuggestionsView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting()) + if (mSuggestionsView.isShowingAddToDictionaryHint()) return true; if (!isShowingSuggestionsStrip()) return false; - if (mApplicationSpecifiedCompletionOn) + if (mInputAttributes.mApplicationSpecifiedCompletionOn) return true; return isSuggestionsRequested(); } @@ -1698,18 +1671,21 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mComposingStateManager.isAutoCorrectionIndicatorOn(); final boolean newAutoCorrectionIndicator = Utils.willAutoCorrect(words); if (oldAutoCorrectionIndicator != newAutoCorrectionIndicator) { - if (LatinImeLogger.sDBG) { + mComposingStateManager.setAutoCorrectionIndicatorOn(newAutoCorrectionIndicator); + if (DEBUG) { Log.d(TAG, "Flip the indicator. " + oldAutoCorrectionIndicator + " -> " + newAutoCorrectionIndicator); + if (newAutoCorrectionIndicator + != mComposingStateManager.isAutoCorrectionIndicatorOn()) { + throw new RuntimeException("Couldn't flip the indicator! We are not " + + "composing a word right now."); + } } - final CharSequence textWithUnderline = newAutoCorrectionIndicator - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( - this, mComposingStringBuilder) - : mComposingStringBuilder; + final CharSequence textWithUnderline = + getTextWithUnderline(mWordComposer.getTypedWord()); if (!TextUtils.isEmpty(textWithUnderline)) { ic.setComposingText(textWithUnderline, 1); } - mComposingStateManager.setAutoCorrectionIndicatorOn(newAutoCorrectionIndicator); } } } @@ -1724,12 +1700,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHandler.cancelUpdateSuggestions(); mHandler.cancelUpdateBigramPredictions(); - if (!mHasUncommittedTypedChars) { + if (!mWordComposer.isComposingWord()) { setPunctuationSuggestions(); return; } - final WordComposer wordComposer = mWordComposer; // TODO: May need a better way of retrieving previous word final InputConnection ic = getCurrentInputConnection(); final CharSequence prevWord; @@ -1739,26 +1714,35 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators); } // getSuggestedWordBuilder handles gracefully a null value of prevWord - final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( - wordComposer, prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo()); + final SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(mWordComposer, + prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); - boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection(); - final CharSequence typedWord = wordComposer.getTypedWord(); + boolean autoCorrectionAvailable = !mInputAttributes.mInputTypeNoAutoCorrect + && mSuggest.hasAutoCorrection(); + final CharSequence typedWord = mWordComposer.getTypedWord(); // Here, we want to promote a whitelisted word if exists. // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" // but still autocorrected from - in the case the whitelist only capitalizes the word. // The whitelist should be case-insensitive, so it's not possible to be consistent with // a boolean flag. Right now this is handled with a slight hack in // WhitelistDictionary#shouldForciblyAutoCorrectFrom. + final int quotesCount = mWordComposer.trailingSingleQuotesCount(); final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected( - mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization()); + mSuggest.getUnigramDictionaries(), + // If the typed string ends with a single quote, for dictionary lookup purposes + // we behave as if the single quote was not here. Here, we are looking up the + // typed string in the dictionary (to avoid autocorrecting from an existing + // word, so for consistency this lookup should be made WITHOUT the trailing + // single quote. + quotesCount > 0 + ? typedWord.subSequence(0, typedWord.length() - quotesCount) : typedWord, + preferCapitalization()); if (mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { autoCorrectionAvailable |= (!allowsToBeAutoCorrected); } // Don't auto-correct words with multiple capital letter - autoCorrectionAvailable &= !wordComposer.isMostlyCaps(); - autoCorrectionAvailable &= !TextEntryState.isRecorrecting(); + autoCorrectionAvailable &= !mWordComposer.isMostlyCaps(); // Basically, we update the suggestion strip only when suggestion count > 1. However, // there is an exception: We update the suggestion strip whenever typed word's length @@ -1794,34 +1778,46 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar setSuggestions(suggestedWords); if (suggestedWords.size() > 0) { if (shouldBlockAutoCorrectionBySafetyNet) { - mBestWord = typedWord; + mWordComposer.setAutoCorrection(typedWord); } else if (suggestedWords.hasAutoCorrectionWord()) { - mBestWord = suggestedWords.getWord(1); + mWordComposer.setAutoCorrection(suggestedWords.getWord(1)); } else { - mBestWord = typedWord; + mWordComposer.setAutoCorrection(typedWord); } } else { - mBestWord = null; + // TODO: replace with mWordComposer.deleteAutoCorrection()? + mWordComposer.setAutoCorrection(null); } setSuggestionStripShown(isSuggestionsStripVisible()); } - private boolean pickDefaultSuggestion(int separatorCode) { + private void commitCurrentAutoCorrection(final int separatorCodePoint, + final InputConnection ic) { // Complete any pending suggestions query first if (mHandler.hasPendingUpdateSuggestions()) { mHandler.cancelUpdateSuggestions(); updateSuggestions(); } - if (mBestWord != null && mBestWord.length() > 0) { - TextEntryState.acceptedDefault(mWordComposer.getTypedWord(), mBestWord, separatorCode); + final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull(); + if (autoCorrection != null) { + final String typedWord = mWordComposer.getTypedWord(); + if (TextUtils.isEmpty(typedWord)) { + throw new RuntimeException("We have an auto-correction but the typed word " + + "is empty? Impossible! I must commit suicide."); + } + Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint); mExpectingUpdateSelection = true; - commitBestWord(mBestWord); + commitChosenWord(autoCorrection, WordComposer.COMMIT_TYPE_DECIDED_WORD); // Add the word to the user unigram dictionary if it's not a known word - addToUserUnigramAndBigramDictionaries(mBestWord, + addToUserUnigramAndBigramDictionaries(autoCorrection, UserUnigramDictionary.FREQUENCY_FOR_TYPED); - return true; + if (!typedWord.equals(autoCorrection) && null != ic) { + // This will make the correction flash for a short while as a visual clue + // to the user that auto-correction happened. + InputConnectionCompatUtils.commitCorrection(ic, + mLastSelectionEnd - typedWord.length(), typedWord, autoCorrection); + } } - return false; } @Override @@ -1831,18 +1827,17 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion, mSettingsValues.mWordSeparators); - final boolean recorrecting = TextEntryState.isRecorrecting(); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } - if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null + if (mInputAttributes.mApplicationSpecifiedCompletionOn + && mApplicationSpecifiedCompletions != null && index >= 0 && index < mApplicationSpecifiedCompletions.length) { if (ic != null) { final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; ic.commitCompletion(completionInfo); } - mCommittedLength = suggestion.length(); if (mSuggestionsView != null) { mSuggestionsView.clear(); } @@ -1861,36 +1856,24 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar LatinImeLogger.logOnManualSuggestion( "", suggestion.toString(), index, suggestions.mWords); // Find out whether the previous character is a space. If it is, as a special case - // for punctuation entered through the suggestion strip, it should be considered - // a magic space even if it was a normal space. This is meant to help in case the user + // for punctuation entered through the suggestion strip, it should be swapped + // if it was a magic or a weak space. This is meant to help in case the user // pressed space on purpose of displaying the suggestion strip punctuation. final int rawPrimaryCode = suggestion.charAt(0); // Maybe apply the "bidi mirrored" conversions for parentheses - final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard(); + final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final boolean isRtl = keyboard != null && keyboard.mIsRtlKeyboard; final int primaryCode = Key.getRtlParenthesisCode(rawPrimaryCode, isRtl); - final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : ""; - final int toLeft = (ic == null || TextUtils.isEmpty(beforeText)) - ? 0 : beforeText.charAt(0); - final boolean oldMagicSpace = mJustAddedMagicSpace; - if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true; - onCodeInput(primaryCode, new int[] { primaryCode }, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE); - mJustAddedMagicSpace = oldMagicSpace; + insertPunctuationFromSuggestionStrip(ic, primaryCode); + // TODO: the following endBatchEdit seems useless, check if (ic != null) { ic.endBatchEdit(); } return; } - if (!mHasUncommittedTypedChars) { - // If we are not composing a word, then it was a suggestion inferred from - // context - no user input. We should reset the word composer. - mWordComposer.reset(); - } mExpectingUpdateSelection = true; - commitBestWord(suggestion); + commitChosenWord(suggestion, WordComposer.COMMIT_TYPE_MANUAL_PICK); // Add the word to the auto dictionary if it's not a known word if (index == 0) { addToUserUnigramAndBigramDictionaries(suggestion, @@ -1898,11 +1881,12 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } else { addToOnlyBigramDictionary(suggestion, 1); } - LatinImeLogger.logOnManualSuggestion(mComposingStringBuilder.toString(), + // TODO: the following is fishy, because it seems there may be cases where we are not + // composing a word at all. Maybe throw an exception if !mWordComposer.isComposingWord() ? + LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(), suggestion.toString(), index, suggestions.mWords); - TextEntryState.acceptedSuggestion(mComposingStringBuilder.toString(), suggestion); // Follow it with a space - if (mInsertSpaceOnPickSuggestionManually && !recorrecting) { + if (mInputAttributes.mInsertSpaceOnPickSuggestionManually) { sendMagicSpace(); } @@ -1922,13 +1906,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar || !AutoCorrection.isValidWord( mSuggest.getUnigramDictionaries(), suggestion, true)); - if (!recorrecting) { - // Fool the state watcher so that a subsequent backspace will not do a revert, unless - // we just did a correction, in which case we need to stay in - // TextEntryState.State.PICKED_SUGGESTION state. - TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true, - WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); - } + Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE, + WordComposer.NOT_A_COORDINATE); if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. @@ -1938,8 +1917,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // take a noticeable delay to update them which may feel uneasy. } if (showingAddToDictionaryHint) { - if (mIsUserDictionaryAvaliable) { - mSuggestionsView.showAddToDictionaryHint(suggestion); + if (mIsUserDictionaryAvailable) { + mSuggestionsView.showAddToDictionaryHint( + suggestion, mSettingsValues.mHintToSaveText); } else { mHandler.postUpdateSuggestions(); } @@ -1952,7 +1932,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar /** * Commits the chosen word to the text field and saves it for later retrieval. */ - private void commitBestWord(CharSequence bestWord) { + private void commitChosenWord(final CharSequence bestWord, final int commitType) { final KeyboardSwitcher switcher = mKeyboardSwitcher; if (!switcher.isKeyboardAvailable()) return; @@ -1967,8 +1947,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar ic.commitText(bestWord, 1); } } - mHasUncommittedTypedChars = false; - mCommittedLength = bestWord.length(); + // TODO: figure out here if this is an auto-correct or if the best word is actually + // what user typed. Note: currently this is done much later in + // WordComposer#didAutoCorrectToAnotherWord by string equality of the remembered + // strings. + mWordComposer.onCommitWord(commitType); } private static final WordComposer sEmptyWordComposer = new WordComposer(); @@ -1984,7 +1967,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), mSettingsValues.mWordSeparators); SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(sEmptyWordComposer, - prevWord, mKeyboardSwitcher.getLatinKeyboard().getProximityInfo()); + prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode); if (builder.size() > 0) { // Explicitly supply an empty typed word (the no-second-arg version of @@ -2070,54 +2053,125 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return false; } - // "ic" must not null - private boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) { + // "ic" must not be null + private static boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) { CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); return TextUtils.equals(text, beforeText); } - // "ic" must not null - private void revertLastWord(final InputConnection ic) { - if (mHasUncommittedTypedChars || mComposingStringBuilder.length() <= 0) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); - return; - } + // "ic" must not be null + /** + * Check if the cursor is actually at the end of a word. If so, restart suggestions on this + * word, else do nothing. + */ + private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( + final InputConnection ic) { + // Bail out if the cursor is not at the end of a word (cursor must be preceded by + // non-whitespace, non-separator, non-start-of-text) + // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. + final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); + if (TextUtils.isEmpty(textBeforeCursor) + || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; + + // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, + // separator or end of line/text) + // Example: "test|"<EOL> "te|st" get rejected here + final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(textAfterCursor) + && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; + + // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) + // Example: " '|" gets rejected here but "I'|" and "I|" are okay + final CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); + if (TextUtils.isEmpty(word)) return; + if (word.length() == 1 && !Character.isLetter(word.charAt(0))) return; + + // Okay, we are at the end of a word. Restart suggestions. + restartSuggestionsOnWordBeforeCursor(ic, word); + } + + // "ic" must not be null + private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, + final CharSequence word) { + mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); + mComposingStateManager.onStartComposingText(); + ic.deleteSurroundingText(word.length(), 0); + ic.setComposingText(word, 1); + mHandler.postUpdateSuggestions(); + } + // "ic" must not be null + private void cancelAutoCorrect(final InputConnection ic) { + mWordComposer.resumeSuggestionOnKeptWord(); + final String originallyTypedWord = mWordComposer.getTypedWord(); + final CharSequence autoCorrectedTo = mWordComposer.getAutoCorrectionOrNull(); + final int cancelLength = autoCorrectedTo.length(); final CharSequence separator = ic.getTextBeforeCursor(1, 0); - ic.deleteSurroundingText(1, 0); - final CharSequence textToTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); - ic.deleteSurroundingText(mCommittedLength, 0); - - // Re-insert "separator" only when the deleted character was word separator and the - // composing text wasn't equal to the auto-corrected text which can be found before - // the cursor. - if (!TextUtils.isEmpty(separator) - && mSettingsValues.isWordSeparator(separator.charAt(0)) - && !TextUtils.equals(mComposingStringBuilder, textToTheLeft)) { - ic.commitText(mComposingStringBuilder, 1); - TextEntryState.acceptedTyped(mComposingStringBuilder); - ic.commitText(separator, 1); - TextEntryState.typedCharacter(separator.charAt(0), true, - WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); - // Clear composing text - mComposingStringBuilder.setLength(0); - } else { - mHasUncommittedTypedChars = true; - ic.setComposingText(mComposingStringBuilder, 1); - TextEntryState.backspace(); + if (DEBUG) { + final String wordBeforeCursor = + ic.getTextBeforeCursor(cancelLength + 1, 0).subSequence(0, cancelLength) + .toString(); + if (!autoCorrectedTo.equals(wordBeforeCursor)) { + throw new RuntimeException("cancelAutoCorrect check failed: we thought we were " + + "reverting \"" + autoCorrectedTo + + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); + } + if (originallyTypedWord.equals(wordBeforeCursor)) { + throw new RuntimeException("cancelAutoCorrect check failed: we wanted to cancel " + + "auto correction and revert to \"" + originallyTypedWord + + "\" but we found this very string before the cursor"); + } } + ic.deleteSurroundingText(cancelLength + 1, 0); + ic.commitText(originallyTypedWord, 1); + // Re-insert the separator + ic.commitText(separator, 1); + mWordComposer.deleteAutoCorrection(); + mWordComposer.onCommitWord(WordComposer.COMMIT_TYPE_CANCEL_AUTO_CORRECT); + Utils.Stats.onSeparator(separator.charAt(0), WordComposer.NOT_A_COORDINATE, + WordComposer.NOT_A_COORDINATE); mHandler.cancelUpdateBigramPredictions(); mHandler.postUpdateSuggestions(); } - // "ic" must not null + // "ic" must not be null + private void restartSuggestionsOnManuallyPickedTypedWord(final InputConnection ic) { + final int restartLength = mWordComposer.size(); + if (DEBUG) { + final String wordBeforeCursor = + ic.getTextBeforeCursor(restartLength + 1, 0).subSequence(0, restartLength) + .toString(); + if (!mWordComposer.getTypedWord().equals(wordBeforeCursor)) { + throw new RuntimeException("restartSuggestionsOnManuallyPickedTypedWord " + + "check failed: we thought we were reverting \"" + + mWordComposer.getTypedWord() + + "\", but before the cursor we found \"" + + wordBeforeCursor + "\""); + } + } + ic.deleteSurroundingText(restartLength + 1, 0); + + // Note: this relies on the last word still being held in the WordComposer + // Note: in the interest of code simplicity, we may want to just call + // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving + // the old WordComposer allows to reuse the actual typed coordinates. + mWordComposer.resumeSuggestionOnKeptWord(); + ic.setComposingText(mWordComposer.getTypedWord(), 1); + mHandler.cancelUpdateBigramPredictions(); + mHandler.postUpdateSuggestions(); + } + + // "ic" must not be null private boolean revertDoubleSpace(final InputConnection ic) { mHandler.cancelDoubleSpacesTimer(); // Here we test whether we indeed have a period and a space before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); - if (!". ".equals(textBeforeCursor)) - return false; + if (!". ".equals(textBeforeCursor)) { + // We should not have come here if we aren't just after a ". ". + throw new RuntimeException("Tried to revert double-space combo but we didn't find " + + "\". \" just before the cursor."); + } ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(" ", 1); @@ -2125,13 +2179,31 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return true; } + private static boolean revertSwapPunctuation(final InputConnection ic) { + // Here we test whether we indeed have a space and something else before us. This should not + // be needed, but it's there just in case something went wrong. + final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); + // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to + // enter surrogate pairs this code will have been removed. + if (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1)) { + // We should not have come here if the text before the cursor is not a space. + throw new RuntimeException("Tried to revert a swap of punctuation but we didn't " + + "find a space just before the cursor."); + } + ic.beginBatchEdit(); + ic.deleteSurroundingText(2, 0); + ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); + ic.endBatchEdit(); + return true; + } + public boolean isWordSeparator(int code) { return mSettingsValues.isWordSeparator(code); } private void sendMagicSpace() { sendKeyChar((char)Keyboard.CODE_SPACE); - mJustAddedMagicSpace = true; + mSpaceState = SPACE_STATE_MAGIC; mKeyboardSwitcher.updateShiftState(); } @@ -2157,12 +2229,16 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar loadSettings(); } + private void hapticAndAudioFeedback(int primaryCode) { + vibrate(); + playKeyClick(primaryCode); + } + @Override public void onPress(int primaryCode, boolean withSliding) { final KeyboardSwitcher switcher = mKeyboardSwitcher; if (switcher.isVibrateAndSoundFeedbackRequired()) { - vibrate(); - playKeyClick(primaryCode); + hapticAndAudioFeedback(primaryCode); } final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { @@ -2200,11 +2276,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } }; - // update keypress sound volume - private void updateSoundEffectVolume() { - mFxVolume = Utils.getCurrentKeypressSoundVolume(mPrefs, mResources); - } - // update flags for silent mode private void updateRingerMode() { if (mAudioManager == null) { @@ -2214,10 +2285,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); } - private void updateKeypressVibrationDuration() { - mKeypressVibrationDuration = Utils.getCurrentVibrationDuration(mPrefs, mResources); - } - private void playKeyClick(int primaryCode) { // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode @@ -2242,7 +2309,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar sound = AudioManager.FX_KEYPRESS_STANDARD; break; } - mAudioManager.playSoundEffect(sound, mFxVolume); + mAudioManager.playSoundEffect(sound, mSettingsValues.mFxVolume); } } @@ -2250,7 +2317,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (!mSettingsValues.mVibrateOn) { return; } - if (mKeypressVibrationDuration < 0) { + if (mSettingsValues.mKeypressVibrationDuration < 0) { // Go ahead with the system default LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) { @@ -2259,12 +2326,12 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } else if (mVibrator != null) { - mVibrator.vibrate(mKeypressVibrationDuration); + mVibrator.vibrate(mSettingsValues.mKeypressVibrationDuration); } } - public WordComposer getCurrentWord() { - return mWordComposer; + public boolean isAutoCapitalized() { + return mWordComposer.isAutoCapitalized(); } boolean isSoundOn() { @@ -2274,22 +2341,14 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private void updateCorrectionMode() { // TODO: cleanup messy flags final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled - && !mInputTypeNoAutoCorrect; - mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled) - ? Suggest.CORRECTION_FULL - : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); - mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect - && mSettingsValues.mAutoCorrectEnabled) + && !mInputAttributes.mInputTypeNoAutoCorrect; + mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE; + mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect) ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; - if (mSuggest != null) { - mSuggest.setCorrectionMode(mCorrectionMode); - } } - private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) { - final String suggestionVisiblityStr = prefs.getString( - Settings.PREF_SHOW_SUGGESTIONS_SETTING, - res.getString(R.string.prefs_suggestion_visibility_default_value)); + private void updateSuggestionVisibility(final Resources res) { + final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting; for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { if (suggestionVisiblityStr.equals(res.getString(visibility))) { mSuggestionVisibility = visibility; @@ -2328,7 +2387,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar switch (position) { case 0: Intent intent = CompatUtils.getInputLanguageSelectionIntent( - mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK + Utils.getInputMethodId(mImm, getPackageName()), + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); @@ -2377,35 +2437,16 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); - p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); - p.println(" mComposingStringBuilder=" + mComposingStringBuilder.toString()); - p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); + final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); + final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; + p.println(" Keyboard mode = " + keyboardMode); + p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn); p.println(" mCorrectionMode=" + mCorrectionMode); - p.println(" mHasUncommittedTypedChars=" + mHasUncommittedTypedChars); + p.println(" isComposingWord=" + mWordComposer.isComposingWord()); p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); - p.println(" mInsertSpaceOnPickSuggestionManually=" + mInsertSpaceOnPickSuggestionManually); - p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); - p.println(" TextEntryState.state=" + TextEntryState.getState()); p.println(" mSoundOn=" + mSettingsValues.mSoundOn); p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); - } - - // Characters per second measurement - - private long mLastCpsTime; - private static final int CPS_BUFFER_SIZE = 16; - private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; - private int mCpsIndex; - - private void measureCps() { - long now = System.currentTimeMillis(); - if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial - mCpsIntervals[mCpsIndex] = now - mLastCpsTime; - mLastCpsTime = now; - mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE; - long total = 0; - for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; - System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); + p.println(" mInputAttributes=" + mInputAttributes.toString()); } } diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index ae8eb374b..6f1adfe71 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -16,11 +16,12 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.latin.Dictionary.DataType; - import android.content.Context; import android.content.SharedPreferences; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.Dictionary.DataType; import java.util.List; @@ -28,12 +29,13 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static boolean sDBG = false; public static boolean sVISUALDEBUG = false; + public static boolean sUsabilityStudy = false; @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { } - public static void init(Context context, SharedPreferences prefs) { + public static void init(LatinIME context, SharedPreferences prefs) { } public static void commit() { @@ -44,7 +46,7 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void logOnManualSuggestion( String before, String after, int position, List<CharSequence> suggestions) { - } + } public static void logOnAutoCorrection(String before, String after, int separatorCode) { } @@ -67,6 +69,9 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void logOnWarning(String warning) { } + public static void onStartInputView(EditorInfo editorInfo) { + } + public static void onStartSuggestion(CharSequence previousWords) { } @@ -78,4 +83,8 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void onPrintAllUsabilityStudyLogs() { } + + public static boolean isResearcherPackage(Context context) { + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 773efe709..5af21452a 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -38,7 +38,6 @@ import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.View; -import android.view.inputmethod.EditorInfo; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; @@ -46,12 +45,10 @@ import android.widget.TextView; import com.android.inputmethod.compat.CompatUtils; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; -import com.android.inputmethod.compat.InputTypeCompatUtils; import com.android.inputmethod.compat.VibratorCompatWrapper; import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethodcommon.InputMethodSettingsActivity; -import java.util.Arrays; import java.util.Locale; public class Settings extends InputMethodSettingsActivity @@ -61,255 +58,39 @@ public class Settings extends InputMethodSettingsActivity public static final boolean ENABLE_EXPERIMENTAL_SETTINGS = false; - public static final String PREF_GENERAL_SETTINGS_KEY = "general_settings"; + // In the same order as xml/prefs.xml + public static final String PREF_GENERAL_SETTINGS = "general_settings"; + public static final String PREF_SUBTYPES_SETTINGS = "subtype_settings"; + public static final String PREF_AUTO_CAP = "auto_cap"; public static final String PREF_VIBRATE_ON = "vibrate_on"; public static final String PREF_SOUND_ON = "sound_on"; - public static final String PREF_KEY_PREVIEW_POPUP_ON = "popup_on"; - public static final String PREF_AUTO_CAP = "auto_cap"; + public static final String PREF_POPUP_ON = "popup_on"; public static final String PREF_SHOW_SETTINGS_KEY = "show_settings_key"; - public static final String PREF_VOICE_SETTINGS_KEY = "voice_mode"; - public static final String PREF_INPUT_LANGUAGE = "input_language"; - public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; - public static final String PREF_SUBTYPES = "subtype_settings"; - + public static final String PREF_VOICE_MODE = "voice_mode"; + public static final String PREF_CORRECTION_SETTINGS = "correction_settings"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; - public static final String PREF_CORRECTION_SETTINGS_KEY = "correction_settings"; - public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; - public static final String PREF_DEBUG_SETTINGS = "debug_settings"; - - public static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; - public static final String PREF_BIGRAM_PREDICTIONS = "bigram_prediction"; - - public static final String PREF_MISC_SETTINGS_KEY = "misc_settings"; - + public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; + public static final String PREF_MISC_SETTINGS = "misc_settings"; + public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; + public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings"; public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY = "pref_key_preview_popup_dismiss_delay"; - public static final String PREF_KEY_USE_CONTACTS_DICT = - "pref_key_use_contacts_dict"; - public static final String PREF_KEY_ENABLE_SPAN_INSERT = - "enable_span_insert"; - - public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; - - public static final String PREF_KEYPRESS_VIBRATION_DURATION_SETTINGS = + public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict"; + public static final String PREF_BIGRAM_SUGGESTION = "bigram_suggestion"; + public static final String PREF_BIGRAM_PREDICTIONS = "bigram_prediction"; + public static final String PREF_KEY_ENABLE_SPAN_INSERT = "enable_span_insert"; + public static final String PREF_VIBRATION_DURATION_SETTINGS = "pref_vibration_duration_settings"; - public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume"; - // Dialog ids - private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; - - public static class Values { - // From resources: - public final int mDelayUpdateOldSuggestions; - public final String mWordSeparators; - public final String mMagicSpaceStrippers; - public final String mMagicSpaceSwappers; - public final String mSuggestPuncs; - public final SuggestedWords mSuggestPuncList; - private final String mSymbolsExcludedFromWordSeparators; - - // From preferences: - public final boolean mSoundOn; // Sound setting private to Latin IME (see mSilentModeOn) - public final boolean mVibrateOn; - public final boolean mKeyPreviewPopupOn; - public final int mKeyPreviewPopupDismissDelay; - public final boolean mAutoCap; - public final boolean mAutoCorrectEnabled; - public final double mAutoCorrectionThreshold; - // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary - public final boolean mBigramSuggestionEnabled; - // Prediction: use bigrams to predict the next word when there is no input for it yet - public final boolean mBigramPredictionEnabled; - public final boolean mUseContactsDict; - public final boolean mEnableSuggestionSpanInsertion; - - private final boolean mShowSettingsKey; - private final boolean mVoiceKeyEnabled; - private final boolean mVoiceKeyOnMain; - - public Values(final SharedPreferences prefs, final Context context, - final String localeStr) { - final Resources res = context.getResources(); - final Locale savedLocale; - if (null != localeStr) { - final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); - savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale); - } else { - savedLocale = null; - } - - // Get the resources - mDelayUpdateOldSuggestions = res.getInteger( - R.integer.config_delay_update_old_suggestions); - mMagicSpaceStrippers = res.getString(R.string.magic_space_stripping_symbols); - mMagicSpaceSwappers = res.getString(R.string.magic_space_swapping_symbols); - String wordSeparators = mMagicSpaceStrippers + mMagicSpaceSwappers - + res.getString(R.string.magic_space_promoting_symbols); - final String symbolsExcludedFromWordSeparators = - res.getString(R.string.symbols_excluded_from_word_separators); - for (int i = symbolsExcludedFromWordSeparators.length() - 1; i >= 0; --i) { - wordSeparators = wordSeparators.replace( - symbolsExcludedFromWordSeparators.substring(i, i + 1), ""); - } - mSymbolsExcludedFromWordSeparators = symbolsExcludedFromWordSeparators; - mWordSeparators = wordSeparators; - mSuggestPuncs = res.getString(R.string.suggested_punctuations); - // TODO: it would be nice not to recreate this each time we change the configuration - mSuggestPuncList = createSuggestPuncList(mSuggestPuncs); - - // Get the settings preferences - final boolean hasVibrator = VibratorCompatWrapper.getInstance(context).hasVibrator(); - mVibrateOn = hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON, - res.getBoolean(R.bool.config_default_vibration_enabled)); - mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, - res.getBoolean(R.bool.config_default_sound_enabled)); - mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res); - mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res); - mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); - mAutoCorrectEnabled = isAutoCorrectEnabled(prefs, res); - mBigramSuggestionEnabled = mAutoCorrectEnabled - && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled); - mBigramPredictionEnabled = mBigramSuggestionEnabled - && isBigramPredictionEnabled(prefs, res); - mAutoCorrectionThreshold = getAutoCorrectionThreshold(prefs, res); - mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true); - mEnableSuggestionSpanInsertion = - prefs.getBoolean(Settings.PREF_KEY_ENABLE_SPAN_INSERT, true); - final boolean defaultShowSettingsKey = res.getBoolean( - R.bool.config_default_show_settings_key); - mShowSettingsKey = isShowSettingsKeyOption(res) - ? prefs.getBoolean(Settings.PREF_SHOW_SETTINGS_KEY, defaultShowSettingsKey) - : defaultShowSettingsKey; - final String voiceModeMain = res.getString(R.string.voice_mode_main); - final String voiceModeOff = res.getString(R.string.voice_mode_off); - final String voiceMode = prefs.getString(PREF_VOICE_SETTINGS_KEY, voiceModeMain); - mVoiceKeyEnabled = voiceMode != null && !voiceMode.equals(voiceModeOff); - mVoiceKeyOnMain = voiceMode != null && voiceMode.equals(voiceModeMain); - - LocaleUtils.setSystemLocale(res, savedLocale); - } - public boolean isSuggestedPunctuation(int code) { - return mSuggestPuncs.contains(String.valueOf((char)code)); - } - - public boolean isWordSeparator(int code) { - return mWordSeparators.contains(String.valueOf((char)code)); - } - - public boolean isSymbolExcludedFromWordSeparators(int code) { - return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code)); - } - - public boolean isMagicSpaceStripper(int code) { - return mMagicSpaceStrippers.contains(String.valueOf((char)code)); - } - - public boolean isMagicSpaceSwapper(int code) { - return mMagicSpaceSwappers.contains(String.valueOf((char)code)); - } - - private static boolean isAutoCorrectEnabled(SharedPreferences sp, Resources resources) { - final String currentAutoCorrectionSetting = sp.getString( - Settings.PREF_AUTO_CORRECTION_THRESHOLD, - resources.getString(R.string.auto_correction_threshold_mode_index_modest)); - final String autoCorrectionOff = resources.getString( - R.string.auto_correction_threshold_mode_index_off); - return !currentAutoCorrectionSetting.equals(autoCorrectionOff); - } - - // Public to access from KeyboardSwitcher. Should it have access to some - // process-global instance instead? - public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) { - final boolean showPopupOption = resources.getBoolean( - R.bool.config_enable_show_popup_on_keypress_option); - if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview); - return sp.getBoolean(Settings.PREF_KEY_PREVIEW_POPUP_ON, - resources.getBoolean(R.bool.config_default_popup_preview)); - } - - // Likewise - public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp, - Resources resources) { - return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, - Integer.toString(resources.getInteger(R.integer.config_delay_after_preview)))); - } - - private static boolean isBigramSuggestionEnabled(SharedPreferences sp, Resources resources, - boolean autoCorrectEnabled) { - final boolean showBigramSuggestionsOption = resources.getBoolean( - R.bool.config_enable_bigram_suggestions_option); - if (!showBigramSuggestionsOption) { - return autoCorrectEnabled; - } - return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, resources.getBoolean( - R.bool.config_default_bigram_suggestions)); - } - - private static boolean isBigramPredictionEnabled(SharedPreferences sp, - Resources resources) { - return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean( - R.bool.config_default_bigram_prediction)); - } - - private static double getAutoCorrectionThreshold(SharedPreferences sp, - Resources resources) { - final String currentAutoCorrectionSetting = sp.getString( - Settings.PREF_AUTO_CORRECTION_THRESHOLD, - resources.getString(R.string.auto_correction_threshold_mode_index_modest)); - final String[] autoCorrectionThresholdValues = resources.getStringArray( - R.array.auto_correction_threshold_values); - // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. - double autoCorrectionThreshold = Double.MAX_VALUE; - try { - final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); - if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { - autoCorrectionThreshold = Double.parseDouble( - autoCorrectionThresholdValues[arrayIndex]); - } - } catch (NumberFormatException e) { - // Whenever the threshold settings are correct, never come here. - autoCorrectionThreshold = Double.MAX_VALUE; - Log.w(TAG, "Cannot load auto correction threshold setting." - + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting - + ", autoCorrectionThresholdValues: " - + Arrays.toString(autoCorrectionThresholdValues)); - } - return autoCorrectionThreshold; - } - - private static SuggestedWords createSuggestPuncList(final String puncs) { - SuggestedWords.Builder builder = new SuggestedWords.Builder(); - if (puncs != null) { - for (int i = 0; i < puncs.length(); i++) { - builder.addWord(puncs.subSequence(i, i + 1)); - } - } - return builder.setIsPunctuationSuggestions().build(); - } - - public static boolean isShowSettingsKeyOption(final Resources resources) { - return resources.getBoolean(R.bool.config_enable_show_settings_key_option); - - } - - public boolean isSettingsKeyEnabled() { - return mShowSettingsKey; - } - - public boolean isVoiceKeyEnabled(EditorInfo attribute) { - final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - final int inputType = (attribute != null) ? attribute.inputType : 0; - return shortcutImeEnabled && mVoiceKeyEnabled - && !InputTypeCompatUtils.isPasswordInputType(inputType); - } + public static final String PREF_INPUT_LANGUAGE = "input_language"; + public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; + public static final String PREF_DEBUG_SETTINGS = "debug_settings"; - public boolean isVoiceKeyOnMain() { - return mVoiceKeyOnMain; - } - } + // Dialog ids + private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; private PreferenceScreen mInputLanguageSelection; private PreferenceScreen mKeypressVibrationDurationSettingsPref; @@ -363,9 +144,9 @@ public class Settings extends InputMethodSettingsActivity final Context context = getActivityInternal(); addPreferencesFromResource(R.xml.prefs); - mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES); + mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES_SETTINGS); mInputLanguageSelection.setOnPreferenceClickListener(this); - mVoicePreference = (ListPreference) findPreference(PREF_VOICE_SETTINGS_KEY); + mVoicePreference = (ListPreference) findPreference(PREF_VOICE_MODE); mShowSettingsKeyPreference = (CheckBoxPreference) findPreference(PREF_SHOW_SETTINGS_KEY); mShowCorrectionSuggestionsPreference = (ListPreference) findPreference(PREF_SHOW_SUGGESTIONS_SETTING); @@ -373,12 +154,12 @@ public class Settings extends InputMethodSettingsActivity prefs.registerOnSharedPreferenceChangeListener(this); mVoiceModeOff = getString(R.string.voice_mode_off); - mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + mVoiceOn = !(prefs.getString(PREF_VOICE_MODE, mVoiceModeOff) .equals(mVoiceModeOff)); mAutoCorrectionThresholdPreference = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD); - mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS); + mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTION); mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS); mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS); if (mDebugSettingsPreference != null) { @@ -391,13 +172,13 @@ public class Settings extends InputMethodSettingsActivity ensureConsistencyOfAutoCorrectionSettings(); final PreferenceGroup generalSettings = - (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS_KEY); + (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS); final PreferenceGroup textCorrectionGroup = - (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY); + (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS); final PreferenceGroup miscSettings = - (PreferenceGroup) findPreference(PREF_MISC_SETTINGS_KEY); + (PreferenceGroup) findPreference(PREF_MISC_SETTINGS); - if (!Values.isShowSettingsKeyOption(res)) { + if (!SettingsValues.isShowSettingsKeyOptionEnabled(res)) { generalSettings.removePreference(mShowSettingsKeyPreference); } @@ -412,13 +193,13 @@ public class Settings extends InputMethodSettingsActivity } if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { - generalSettings.removePreference(findPreference(PREF_SUBTYPES)); + generalSettings.removePreference(findPreference(PREF_SUBTYPES_SETTINGS)); } final boolean showPopupOption = res.getBoolean( R.bool.config_enable_show_popup_on_keypress_option); if (!showPopupOption) { - generalSettings.removePreference(findPreference(PREF_KEY_PREVIEW_POPUP_ON)); + generalSettings.removePreference(findPreference(PREF_POPUP_ON)); } final boolean showBigramSuggestionsOption = res.getBoolean( @@ -444,7 +225,8 @@ public class Settings extends InputMethodSettingsActivity if (null == mKeyPreviewPopupDismissDelay.getValue()) { mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); } - mKeyPreviewPopupDismissDelay.setEnabled(Values.isKeyPreviewPopupEnabled(prefs, res)); + mKeyPreviewPopupDismissDelay.setEnabled( + SettingsValues.isKeyPreviewPopupEnabled(prefs, res)); final PreferenceScreen dictionaryLink = (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY); @@ -455,17 +237,26 @@ public class Settings extends InputMethodSettingsActivity textCorrectionGroup.removePreference(dictionaryLink); } - final boolean showUsabilityModeStudyOption = res.getBoolean( - R.bool.config_enable_usability_study_mode_option); - if (!showUsabilityModeStudyOption || !ENABLE_EXPERIMENTAL_SETTINGS) { - final Preference pref = findPreference(PREF_USABILITY_STUDY_MODE); - if (pref != null) { - miscSettings.removePreference(pref); + final boolean isResearcherPackage = LatinImeLogger.isResearcherPackage(this); + final boolean showUsabilityStudyModeOption = + res.getBoolean(R.bool.config_enable_usability_study_mode_option) + || isResearcherPackage || ENABLE_EXPERIMENTAL_SETTINGS; + final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE); + if (!showUsabilityStudyModeOption) { + if (usabilityStudyPref != null) { + miscSettings.removePreference(usabilityStudyPref); + } + } + if (isResearcherPackage) { + if (usabilityStudyPref instanceof CheckBoxPreference) { + CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref; + checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, true)); + checkbox.setSummary(R.string.settings_warning_researcher_mode); } } mKeypressVibrationDurationSettingsPref = - (PreferenceScreen) findPreference(PREF_KEYPRESS_VIBRATION_DURATION_SETTINGS); + (PreferenceScreen) findPreference(PREF_VIBRATION_DURATION_SETTINGS); if (mKeypressVibrationDurationSettingsPref != null) { mKeypressVibrationDurationSettingsPref.setOnPreferenceClickListener( new OnPreferenceClickListener() { @@ -521,20 +312,20 @@ public class Settings extends InputMethodSettingsActivity public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { (new BackupManager(getActivityInternal())).dataChanged(); // If turning on voice input, show dialog - if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) { - if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + if (key.equals(PREF_VOICE_MODE) && !mVoiceOn) { + if (!prefs.getString(PREF_VOICE_MODE, mVoiceModeOff) .equals(mVoiceModeOff)) { showVoiceConfirmation(); } - } else if (key.equals(PREF_KEY_PREVIEW_POPUP_ON)) { + } else if (key.equals(PREF_POPUP_ON)) { final ListPreference popupDismissDelay = (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); if (null != popupDismissDelay) { - popupDismissDelay.setEnabled(prefs.getBoolean(PREF_KEY_PREVIEW_POPUP_ON, true)); + popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true)); } } ensureConsistencyOfAutoCorrectionSettings(); - mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + mVoiceOn = !(prefs.getString(PREF_VOICE_MODE, mVoiceModeOff) .equals(mVoiceModeOff)); updateVoiceModeSummary(); updateShowCorrectionSuggestionsSummary(); @@ -660,7 +451,7 @@ public class Settings extends InputMethodSettingsActivity SharedPreferences sp, Resources res) { if (mKeypressVibrationDurationSettingsPref != null) { mKeypressVibrationDurationSettingsPref.setSummary( - Utils.getCurrentVibrationDuration(sp, res) + SettingsValues.getCurrentVibrationDuration(sp, res) + res.getString(R.string.settings_ms)); } } @@ -676,7 +467,7 @@ public class Settings extends InputMethodSettingsActivity public void onClick(DialogInterface dialog, int whichButton) { final int ms = Integer.valueOf( mKeypressVibrationDurationSettingsTextView.getText().toString()); - sp.edit().putInt(Settings.PREF_KEYPRESS_VIBRATION_DURATION_SETTINGS, ms).apply(); + sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, ms).apply(); updateKeypressVibrationDurationSettingsSummary(sp, res); } }); @@ -688,7 +479,7 @@ public class Settings extends InputMethodSettingsActivity }); final View v = context.getLayoutInflater().inflate( R.layout.vibration_settings_dialog, null); - final int currentMs = Utils.getCurrentVibrationDuration( + final int currentMs = SettingsValues.getCurrentVibrationDuration( getPreferenceManager().getSharedPreferences(), getResources()); mKeypressVibrationDurationSettingsTextView = (TextView)v.findViewById(R.id.vibration_value); final SeekBar sb = (SeekBar)v.findViewById(R.id.vibration_settings); @@ -717,8 +508,8 @@ public class Settings extends InputMethodSettingsActivity private void updateKeypressSoundVolumeSummary(SharedPreferences sp, Resources res) { if (mKeypressSoundVolumeSettingsPref != null) { - mKeypressSoundVolumeSettingsPref.setSummary( - String.valueOf((int)(Utils.getCurrentKeypressSoundVolume(sp, res) * 100))); + mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf( + (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100))); } } @@ -747,8 +538,8 @@ public class Settings extends InputMethodSettingsActivity }); final View v = context.getLayoutInflater().inflate( R.layout.sound_effect_volume_dialog, null); - final int currentVolumeInt = (int)(Utils.getCurrentKeypressSoundVolume( - getPreferenceManager().getSharedPreferences(), getResources()) * 100); + final int currentVolumeInt = + (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100); mKeypressSoundVolumeSettingsTextView = (TextView)v.findViewById(R.id.sound_effect_volume_value); final SeekBar sb = (SeekBar)v.findViewById(R.id.sound_effect_volume_bar); @@ -774,4 +565,4 @@ public class Settings extends InputMethodSettingsActivity builder.setView(v); builder.create().show(); } -}
\ No newline at end of file +} diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java new file mode 100644 index 000000000..83b27f7fe --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.util.Log; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.compat.InputTypeCompatUtils; +import com.android.inputmethod.compat.VibratorCompatWrapper; + +import java.util.Arrays; +import java.util.Locale; + +public class SettingsValues { + private static final String TAG = SettingsValues.class.getSimpleName(); + + // From resources: + public final int mDelayUpdateOldSuggestions; + public final String mMagicSpaceStrippers; + public final String mMagicSpaceSwappers; + public final String mSuggestPuncs; + public final SuggestedWords mSuggestPuncList; + private final String mSymbolsExcludedFromWordSeparators; + public final String mWordSeparators; + public final CharSequence mHintToSaveText; + public final boolean mUseFullScreenMode; + + // From preferences, in the same order as xml/prefs.xml: + public final boolean mAutoCap; + public final boolean mVibrateOn; + public final boolean mSoundOn; + public final boolean mKeyPreviewPopupOn; + private final boolean mShowSettingsKey; + private final String mVoiceMode; + private final String mAutoCorrectionThresholdRawValue; + public final String mShowSuggestionsSetting; + @SuppressWarnings("unused") // TODO: Use this + private final boolean mUsabilityStudyMode; + @SuppressWarnings("unused") // TODO: Use this + private final String mKeyPreviewPopupDismissDelayRawValue; + public final boolean mUseContactsDict; + // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary + public final boolean mBigramSuggestionEnabled; + // Prediction: use bigrams to predict the next word when there is no input for it yet + public final boolean mBigramPredictionEnabled; + public final boolean mEnableSuggestionSpanInsertion; + @SuppressWarnings("unused") // TODO: Use this + private final int mVibrationDurationSettingsRawValue; + @SuppressWarnings("unused") // TODO: Use this + private final float mKeypressSoundVolumeRawValue; + + // Deduced settings + public final int mKeypressVibrationDuration; + public final float mFxVolume; + public final int mKeyPreviewPopupDismissDelay; + public final boolean mAutoCorrectEnabled; + public final double mAutoCorrectionThreshold; + private final boolean mVoiceKeyEnabled; + private final boolean mVoiceKeyOnMain; + + public SettingsValues(final SharedPreferences prefs, final Context context, + final String localeStr) { + final Resources res = context.getResources(); + final Locale savedLocale; + if (null != localeStr) { + final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr); + savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale); + } else { + savedLocale = null; + } + + // Get the resources + mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions); + mMagicSpaceStrippers = res.getString(R.string.magic_space_stripping_symbols); + mMagicSpaceSwappers = res.getString(R.string.magic_space_swapping_symbols); + if (LatinImeLogger.sDBG) { + for (int i = 0; i < mMagicSpaceStrippers.length(); ++i) { + if (isMagicSpaceSwapper(mMagicSpaceStrippers.codePointAt(i))) { + throw new RuntimeException("Char code " + mMagicSpaceStrippers.codePointAt(i) + + " is both a magic space swapper and stripper."); + } + } + } + mSuggestPuncs = res.getString(R.string.suggested_punctuations); + // TODO: it would be nice not to recreate this each time we change the configuration + mSuggestPuncList = createSuggestPuncList(mSuggestPuncs); + mSymbolsExcludedFromWordSeparators = + res.getString(R.string.symbols_excluded_from_word_separators); + mWordSeparators = createWordSeparators(mMagicSpaceStrippers, mMagicSpaceSwappers, + mSymbolsExcludedFromWordSeparators, res); + mHintToSaveText = context.getText(R.string.hint_add_to_dictionary); + mUseFullScreenMode = res.getBoolean(R.bool.config_use_fullscreen_mode); + + // Get the settings preferences + mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); + mVibrateOn = isVibrateOn(context, prefs, res); + mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, + res.getBoolean(R.bool.config_default_sound_enabled)); + mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res); + mShowSettingsKey = isSettingsKeyShown(prefs, res); + final String voiceModeMain = res.getString(R.string.voice_mode_main); + final String voiceModeOff = res.getString(R.string.voice_mode_off); + mVoiceMode = prefs.getString(Settings.PREF_VOICE_MODE, voiceModeMain); + mAutoCorrectionThresholdRawValue = prefs.getString(Settings.PREF_AUTO_CORRECTION_THRESHOLD, + res.getString(R.string.auto_correction_threshold_mode_index_modest)); + mShowSuggestionsSetting = prefs.getString(Settings.PREF_SHOW_SUGGESTIONS_SETTING, + res.getString(R.string.prefs_suggestion_visibility_default_value)); + mUsabilityStudyMode = getUsabilityStudyMode(prefs); + mKeyPreviewPopupDismissDelayRawValue = prefs.getString( + Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, + Integer.toString(res.getInteger(R.integer.config_delay_after_preview))); + mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true); + mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue); + mBigramSuggestionEnabled = mAutoCorrectEnabled + && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled); + mBigramPredictionEnabled = mBigramSuggestionEnabled + && isBigramPredictionEnabled(prefs, res); + mEnableSuggestionSpanInsertion = + prefs.getBoolean(Settings.PREF_KEY_ENABLE_SPAN_INSERT, true); + mVibrationDurationSettingsRawValue = + prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1); + mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); + + // Compute other readable settings + mKeypressVibrationDuration = getCurrentVibrationDuration(prefs, res); + mFxVolume = getCurrentKeypressSoundVolume(prefs, res); + mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res); + mAutoCorrectionThreshold = getAutoCorrectionThreshold(res, + mAutoCorrectionThresholdRawValue); + mVoiceKeyEnabled = mVoiceMode != null && !mVoiceMode.equals(voiceModeOff); + mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain); + + LocaleUtils.setSystemLocale(res, savedLocale); + } + + // Helper functions to create member values. + private static SuggestedWords createSuggestPuncList(final String puncs) { + SuggestedWords.Builder builder = new SuggestedWords.Builder(); + if (puncs != null) { + for (int i = 0; i < puncs.length(); i++) { + builder.addWord(puncs.subSequence(i, i + 1)); + } + } + return builder.setIsPunctuationSuggestions().build(); + } + + private static String createWordSeparators(final String magicSpaceStrippers, + final String magicSpaceSwappers, final String symbolsExcludedFromWordSeparators, + final Resources res) { + String wordSeparators = magicSpaceStrippers + magicSpaceSwappers + + res.getString(R.string.magic_space_promoting_symbols); + for (int i = symbolsExcludedFromWordSeparators.length() - 1; i >= 0; --i) { + wordSeparators = wordSeparators.replace( + symbolsExcludedFromWordSeparators.substring(i, i + 1), ""); + } + return wordSeparators; + } + + private static boolean isSettingsKeyShown(final SharedPreferences prefs, final Resources res) { + final boolean defaultShowSettingsKey = res.getBoolean( + R.bool.config_default_show_settings_key); + return isShowSettingsKeyOptionEnabled(res) + ? prefs.getBoolean(Settings.PREF_SHOW_SETTINGS_KEY, defaultShowSettingsKey) + : defaultShowSettingsKey; + } + + public static boolean isShowSettingsKeyOptionEnabled(final Resources resources) { + // TODO: Read this once and for all into a public final member + return resources.getBoolean(R.bool.config_enable_show_settings_key_option); + } + + private static boolean isVibrateOn(final Context context, final SharedPreferences prefs, + final Resources res) { + final boolean hasVibrator = VibratorCompatWrapper.getInstance(context).hasVibrator(); + return hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON, + res.getBoolean(R.bool.config_default_vibration_enabled)); + } + + public boolean isSuggestedPunctuation(int code) { + return mSuggestPuncs.contains(String.valueOf((char)code)); + } + + public boolean isWordSeparator(int code) { + return mWordSeparators.contains(String.valueOf((char)code)); + } + + public boolean isSymbolExcludedFromWordSeparators(int code) { + return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code)); + } + + public boolean isMagicSpaceStripper(int code) { + return mMagicSpaceStrippers.contains(String.valueOf((char)code)); + } + + public boolean isMagicSpaceSwapper(int code) { + return mMagicSpaceSwappers.contains(String.valueOf((char)code)); + } + + private static boolean isAutoCorrectEnabled(final Resources resources, + final String currentAutoCorrectionSetting) { + final String autoCorrectionOff = resources.getString( + R.string.auto_correction_threshold_mode_index_off); + return !currentAutoCorrectionSetting.equals(autoCorrectionOff); + } + + // Public to access from KeyboardSwitcher. Should it have access to some + // process-global instance instead? + public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) { + final boolean showPopupOption = resources.getBoolean( + R.bool.config_enable_show_popup_on_keypress_option); + if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview); + return sp.getBoolean(Settings.PREF_POPUP_ON, + resources.getBoolean(R.bool.config_default_popup_preview)); + } + + // Likewise + public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp, + Resources resources) { + // TODO: use mKeyPreviewPopupDismissDelayRawValue instead of reading it again here. + return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, + Integer.toString(resources.getInteger(R.integer.config_delay_after_preview)))); + } + + private static boolean isBigramSuggestionEnabled(final SharedPreferences sp, + final Resources resources, final boolean autoCorrectEnabled) { + final boolean showBigramSuggestionsOption = resources.getBoolean( + R.bool.config_enable_bigram_suggestions_option); + if (!showBigramSuggestionsOption) { + return autoCorrectEnabled; + } + return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTION, resources.getBoolean( + R.bool.config_default_bigram_suggestions)); + } + + private static boolean isBigramPredictionEnabled(final SharedPreferences sp, + final Resources resources) { + return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean( + R.bool.config_default_bigram_prediction)); + } + + private static double getAutoCorrectionThreshold(final Resources resources, + final String currentAutoCorrectionSetting) { + final String[] autoCorrectionThresholdValues = resources.getStringArray( + R.array.auto_correction_threshold_values); + // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. + double autoCorrectionThreshold = Double.MAX_VALUE; + try { + final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); + if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { + autoCorrectionThreshold = Double.parseDouble( + autoCorrectionThresholdValues[arrayIndex]); + } + } catch (NumberFormatException e) { + // Whenever the threshold settings are correct, never come here. + autoCorrectionThreshold = Double.MAX_VALUE; + Log.w(TAG, "Cannot load auto correction threshold setting." + + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting + + ", autoCorrectionThresholdValues: " + + Arrays.toString(autoCorrectionThresholdValues)); + } + return autoCorrectionThreshold; + } + + public boolean isSettingsKeyEnabled() { + return mShowSettingsKey; + } + + public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) { + final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); + final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; + return shortcutImeEnabled && mVoiceKeyEnabled + && !InputTypeCompatUtils.isPasswordInputType(inputType); + } + + public boolean isVoiceKeyOnMain() { + return mVoiceKeyOnMain; + } + + // Accessed from the settings interface, hence public + public static float getCurrentKeypressSoundVolume(final SharedPreferences sp, + final Resources res) { + // TODO: use mVibrationDurationSettingsRawValue instead of reading it again here + final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); + if (volume >= 0) { + return volume; + } + + final String[] volumePerHardwareList = res.getStringArray(R.array.keypress_volumes); + final String hardwarePrefix = Build.HARDWARE + ","; + for (final String element : volumePerHardwareList) { + if (element.startsWith(hardwarePrefix)) { + return Float.parseFloat(element.substring(element.lastIndexOf(',') + 1)); + } + } + return -1.0f; + } + + // Likewise + public static int getCurrentVibrationDuration(final SharedPreferences sp, + final Resources res) { + // TODO: use mKeypressVibrationDuration instead of reading it again here + final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1); + if (ms >= 0) { + return ms; + } + final String[] durationPerHardwareList = res.getStringArray( + R.array.keypress_vibration_durations); + final String hardwarePrefix = Build.HARDWARE + ","; + for (final String element : durationPerHardwareList) { + if (element.startsWith(hardwarePrefix)) { + return (int)Long.parseLong(element.substring(element.lastIndexOf(',') + 1)); + } + } + return -1; + } + + // Likewise + public static boolean getUsabilityStudyMode(final SharedPreferences prefs) { + // TODO: use mUsabilityStudyMode instead of reading it again here + return prefs.getBoolean(Settings.PREF_USABILITY_STUDY_MODE, true); + } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index 8a4862094..42111822d 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -35,7 +35,6 @@ import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.keyboard.KeyboardSwitcher; -import com.android.inputmethod.keyboard.LatinKeyboard; import java.util.ArrayList; import java.util.Arrays; @@ -421,11 +420,7 @@ public class SubtypeSwitcher { ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); mIsNetworkConnected = !noConnection; - final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); - final LatinKeyboard keyboard = switcher.getLatinKeyboard(); - if (keyboard != null) { - keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView()); - } + KeyboardSwitcher.getInstance().onNetworkStateChanged(); } ////////////////////////////////// diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index caa5aac51..e9ca390d3 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -20,6 +20,7 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.ProximityInfo; import java.io.File; @@ -42,9 +43,8 @@ public class Suggest implements Dictionary.WordCallback { public static final int APPROX_MAX_WORD_LENGTH = 32; public static final int CORRECTION_NONE = 0; - public static final int CORRECTION_BASIC = 1; - public static final int CORRECTION_FULL = 2; - public static final int CORRECTION_FULL_BIGRAM = 3; + public static final int CORRECTION_FULL = 1; + public static final int CORRECTION_FULL_BIGRAM = 2; /** * Words that appear in both bigram and unigram data gets multiplier ranging from @@ -101,13 +101,12 @@ public class Suggest implements Dictionary.WordCallback { private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>(); - private CharSequence mTypedWord; + private CharSequence mConsideredWord; // TODO: Remove these member variables by passing more context to addWord() callback method private boolean mIsFirstCharCapitalized; private boolean mIsAllUpperCase; - - private int mCorrectionMode = CORRECTION_BASIC; + private int mTrailingSingleQuotesCount; public Suggest(final Context context, final int dictionaryResId, final Locale locale) { initAsynchronously(context, dictionaryResId, locale); @@ -144,7 +143,7 @@ public class Suggest implements Dictionary.WordCallback { initWhitelistAndAutocorrectAndPool(context, locale); } - private void addOrReplaceDictionary(Map<String, Dictionary> dictionaries, String key, + private static void addOrReplaceDictionary(Map<String, Dictionary> dictionaries, String key, Dictionary dict) { final Dictionary oldDict = (dict == null) ? dictionaries.remove(key) @@ -169,14 +168,6 @@ public class Suggest implements Dictionary.WordCallback { }.start(); } - public int getCorrectionMode() { - return mCorrectionMode; - } - - public void setCorrectionMode(int mode) { - mCorrectionMode = mode; - } - // The main dictionary could have been loaded asynchronously. Don't cache the return value // of this method. public boolean hasMainDictionary() { @@ -255,9 +246,10 @@ public class Suggest implements Dictionary.WordCallback { * @return suggested words object. */ public SuggestedWords getSuggestions(final WordComposer wordComposer, - final CharSequence prevWordForBigram, final ProximityInfo proximityInfo) { + final CharSequence prevWordForBigram, final ProximityInfo proximityInfo, + final int correctionMode) { return getSuggestedWordBuilder(wordComposer, prevWordForBigram, - proximityInfo).build(); + proximityInfo, correctionMode).build(); } private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) { @@ -290,25 +282,27 @@ public class Suggest implements Dictionary.WordCallback { // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder public SuggestedWords.Builder getSuggestedWordBuilder( final WordComposer wordComposer, CharSequence prevWordForBigram, - final ProximityInfo proximityInfo) { + final ProximityInfo proximityInfo, final int correctionMode) { LatinImeLogger.onStartSuggestion(prevWordForBigram); mAutoCorrection.init(); mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); + mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); collectGarbage(mSuggestions, mPrefMaxSuggestions); Arrays.fill(mScores, 0); - // Save a lowercase version of the original word - String typedWord = wordComposer.getTypedWord(); + final String typedWord = wordComposer.getTypedWord(); + final String consideredWord = mTrailingSingleQuotesCount > 0 + ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount) + : typedWord; if (typedWord != null) { // Treating USER_TYPED as UNIGRAM suggestion for logging now. LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.DataType.UNIGRAM); } - mTypedWord = typedWord; + mConsideredWord = consideredWord; - if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM - || mCorrectionMode == CORRECTION_BASIC)) { + if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) { // At first character typed, search only the bigrams Arrays.fill(mBigramScores, 0); collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS); @@ -321,7 +315,7 @@ public class Suggest implements Dictionary.WordCallback { for (final Dictionary dictionary : mBigramDictionaries.values()) { dictionary.getBigrams(wordComposer, prevWordForBigram, this); } - if (TextUtils.isEmpty(typedWord)) { + if (TextUtils.isEmpty(consideredWord)) { // Nothing entered: return all bigrams for the previous word int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions); for (int i = 0; i < insertCount; ++i) { @@ -330,7 +324,7 @@ public class Suggest implements Dictionary.WordCallback { } else { // Word entered: return only bigrams that match the first char of the typed word @SuppressWarnings("null") - final char currentChar = typedWord.charAt(0); + final char currentChar = consideredWord.charAt(0); // TODO: Must pay attention to locale when changing case. final char currentCharUpper = Character.toUpperCase(currentChar); int count = 0; @@ -354,24 +348,41 @@ public class Suggest implements Dictionary.WordCallback { if (key.equals(DICT_KEY_USER_UNIGRAM) || key.equals(DICT_KEY_WHITELIST)) continue; final Dictionary dictionary = mUnigramDictionaries.get(key); - dictionary.getWords(wordComposer, this, proximityInfo); + if (mTrailingSingleQuotesCount > 0) { + final WordComposer tmpWordComposer = new WordComposer(wordComposer); + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { + tmpWordComposer.deleteLast(); + } + dictionary.getWords(tmpWordComposer, this, proximityInfo); + } else { + dictionary.getWords(wordComposer, this, proximityInfo); + } } } - final String typedWordString = typedWord == null ? null : typedWord.toString(); + final String consideredWordString = + consideredWord == null ? null : consideredWord.toString(); CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized, - mWhiteListDictionary.getWhitelistedWord(typedWordString)); + mWhiteListDictionary.getWhitelistedWord(consideredWordString)); mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer, - mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode, + mSuggestions, mScores, consideredWord, mAutoCorrectionThreshold, correctionMode, whitelistedWord); if (whitelistedWord != null) { - mSuggestions.add(0, whitelistedWord); + if (mTrailingSingleQuotesCount > 0) { + final StringBuilder sb = new StringBuilder(whitelistedWord); + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { + sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); + } + mSuggestions.add(0, sb.toString()); + } else { + mSuggestions.add(0, whitelistedWord); + } } if (typedWord != null) { - mSuggestions.add(0, typedWordString); + mSuggestions.add(0, typedWord.toString()); } Utils.removeDupes(mSuggestions); @@ -424,7 +435,7 @@ public class Suggest implements Dictionary.WordCallback { int pos = 0; // Check if it's the same word, only caps are different - if (Utils.equalsIgnoreCase(mTypedWord, word, offset, length)) { + if (Utils.equalsIgnoreCase(mConsideredWord, word, offset, length)) { // TODO: remove this surrounding if clause and move this logic to // getSuggestedWordBuilder. if (suggestions.size() > 0) { @@ -486,6 +497,9 @@ public class Suggest implements Dictionary.WordCallback { } else { sb.append(word, offset, length); } + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { + sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); + } suggestions.add(pos, sb); if (suggestions.size() > prefMaxSuggestions) { final CharSequence garbage = suggestions.remove(prefMaxSuggestions); @@ -518,7 +532,8 @@ public class Suggest implements Dictionary.WordCallback { return -1; } - private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) { + private static void collectGarbage(ArrayList<CharSequence> suggestions, + int prefMaxSuggestions) { int poolSize = StringBuilderPool.getSize(); int garbageSize = suggestions.size(); while (poolSize < prefMaxSuggestions && garbageSize > 0) { diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java deleted file mode 100644 index 79b3bdebb..000000000 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * 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.keyboard.Keyboard; -import com.android.inputmethod.latin.Utils.RingCharBuffer; - -import android.util.Log; - -public class TextEntryState { - private static final String TAG = TextEntryState.class.getSimpleName(); - private static final boolean DEBUG = false; - - private static final int UNKNOWN = 0; - private static final int START = 1; - private static final int IN_WORD = 2; - private static final int ACCEPTED_DEFAULT = 3; - private static final int PICKED_SUGGESTION = 4; - private static final int PUNCTUATION_AFTER_WORD = 5; - private static final int PUNCTUATION_AFTER_ACCEPTED = 6; - private static final int SPACE_AFTER_ACCEPTED = 7; - private static final int SPACE_AFTER_PICKED = 8; - private static final int UNDO_COMMIT = 9; - private static final int RECORRECTING = 10; - private static final int PICKED_RECORRECTION = 11; - - private static int sState = UNKNOWN; - private static int sPreviousState = UNKNOWN; - - private static void setState(final int newState) { - sPreviousState = sState; - sState = newState; - } - - public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord, - int separatorCode) { - if (typedWord == null) return; - setState(ACCEPTED_DEFAULT); - LatinImeLogger.logOnAutoCorrection( - typedWord.toString(), actualWord.toString(), separatorCode); - if (DEBUG) - displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord); - } - - // State.ACCEPTED_DEFAULT will be changed to other sub-states - // (see "case ACCEPTED_DEFAULT" in typedCharacter() below), - // and should be restored back to State.ACCEPTED_DEFAULT after processing for each sub-state. - public static void backToAcceptedDefault(CharSequence typedWord) { - if (typedWord == null) return; - switch (sState) { - case SPACE_AFTER_ACCEPTED: - case PUNCTUATION_AFTER_ACCEPTED: - case IN_WORD: - setState(ACCEPTED_DEFAULT); - break; - default: - break; - } - if (DEBUG) displayState("backToAcceptedDefault", "typedWord", typedWord); - } - - public static void acceptedTyped(CharSequence typedWord) { - setState(PICKED_SUGGESTION); - if (DEBUG) displayState("acceptedTyped", "typedWord", typedWord); - } - - public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) { - if (sState == RECORRECTING || sState == PICKED_RECORRECTION) { - setState(PICKED_RECORRECTION); - } else { - setState(PICKED_SUGGESTION); - } - if (DEBUG) - displayState("acceptedSuggestion", "typedWord", typedWord, "actualWord", actualWord); - } - - public static void selectedForRecorrection() { - setState(RECORRECTING); - if (DEBUG) displayState("selectedForRecorrection"); - } - - public static void onAbortRecorrection() { - if (sState == RECORRECTING || sState == PICKED_RECORRECTION) { - setState(START); - } - if (DEBUG) displayState("onAbortRecorrection"); - } - - public static void typedCharacter(char c, boolean isSeparator, int x, int y) { - final boolean isSpace = (c == Keyboard.CODE_SPACE); - switch (sState) { - case IN_WORD: - if (isSpace || isSeparator) { - setState(START); - } else { - // State hasn't changed. - } - break; - case ACCEPTED_DEFAULT: - case SPACE_AFTER_PICKED: - case PUNCTUATION_AFTER_ACCEPTED: - if (isSpace) { - setState(SPACE_AFTER_ACCEPTED); - } else if (isSeparator) { - // Swap - setState(PUNCTUATION_AFTER_ACCEPTED); - } else { - setState(IN_WORD); - } - break; - case PICKED_SUGGESTION: - case PICKED_RECORRECTION: - if (isSpace) { - setState(SPACE_AFTER_PICKED); - } else if (isSeparator) { - // Swap - setState(PUNCTUATION_AFTER_ACCEPTED); - } else { - setState(IN_WORD); - } - break; - case START: - case UNKNOWN: - case SPACE_AFTER_ACCEPTED: - case PUNCTUATION_AFTER_WORD: - if (!isSpace && !isSeparator) { - setState(IN_WORD); - } else { - setState(START); - } - break; - case UNDO_COMMIT: - if (isSpace || isSeparator) { - setState(START); - } else { - setState(IN_WORD); - } - break; - case RECORRECTING: - setState(START); - break; - } - RingCharBuffer.getInstance().push(c, x, y); - if (isSeparator) { - LatinImeLogger.logOnInputSeparator(); - } else { - LatinImeLogger.logOnInputChar(); - } - if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator); - } - - public static void backspace() { - if (sState == ACCEPTED_DEFAULT) { - setState(UNDO_COMMIT); - LatinImeLogger.logOnAutoCorrectionCancelled(); - } else if (sState == UNDO_COMMIT) { - setState(IN_WORD); - } - if (DEBUG) displayState("backspace"); - } - - public static void reset() { - setState(START); - if (DEBUG) displayState("reset"); - } - - public static boolean isAcceptedDefault() { - return sState == ACCEPTED_DEFAULT; - } - - public static boolean isSpaceAfterPicked() { - return sState == SPACE_AFTER_PICKED; - } - - public static boolean isUndoCommit() { - return sState == UNDO_COMMIT; - } - - public static boolean isPunctuationAfterAccepted() { - return sState == PUNCTUATION_AFTER_ACCEPTED; - } - - public static boolean isRecorrecting() { - return sState == RECORRECTING || sState == PICKED_RECORRECTION; - } - - public static String getState() { - return stateName(sState); - } - - private static String stateName(int state) { - switch (state) { - case START: return "START"; - case IN_WORD: return "IN_WORD"; - case ACCEPTED_DEFAULT: return "ACCEPTED_DEFAULT"; - case PICKED_SUGGESTION: return "PICKED_SUGGESTION"; - case PUNCTUATION_AFTER_WORD: return "PUNCTUATION_AFTER_WORD"; - case PUNCTUATION_AFTER_ACCEPTED: return "PUNCTUATION_AFTER_ACCEPTED"; - case SPACE_AFTER_ACCEPTED: return "SPACE_AFTER_ACCEPTED"; - case SPACE_AFTER_PICKED: return "SPACE_AFTER_PICKED"; - case UNDO_COMMIT: return "UNDO_COMMIT"; - case RECORRECTING: return "RECORRECTING"; - case PICKED_RECORRECTION: return "PICKED_RECORRECTION"; - default: return "UNKNOWN"; - } - } - - private static void displayState(String title, Object ... args) { - final StringBuilder sb = new StringBuilder(title); - sb.append(':'); - for (int i = 0; i < args.length; i += 2) { - sb.append(' '); - sb.append(args[i]); - sb.append('='); - sb.append(args[i+1].toString()); - } - sb.append(" state="); - sb.append(stateName(sState)); - sb.append(" previous="); - sb.append(stateName(sPreviousState)); - Log.d(TAG, sb.toString()); - } -} diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java index 9e656675e..f80534cb5 100644 --- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -159,7 +159,7 @@ public class UserBigramDictionary extends ExpandableDictionary { */ public int addBigrams(String word1, String word2) { // remove caps if second word is autocapitalized - if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) { + if (mIme != null && mIme.isAutoCapitalized()) { word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1); } // Do not insert a word as a bigram of itself @@ -238,7 +238,7 @@ public class UserBigramDictionary extends ExpandableDictionary { /** * Query the database */ - private Cursor query(String selection, String[] selectionArgs) { + private static Cursor query(String selection, String[] selectionArgs) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); // main INNER JOIN frequency ON (main._id=freq.pair_id) @@ -310,7 +310,7 @@ public class UserBigramDictionary extends ExpandableDictionary { } /** Prune any old data if the database is getting too big. */ - private void checkPruneData(SQLiteDatabase db) { + private static void checkPruneData(SQLiteDatabase db) { db.execSQL("PRAGMA foreign_keys = ON;"); Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID }, null, null, null, null, null); @@ -380,7 +380,7 @@ public class UserBigramDictionary extends ExpandableDictionary { return null; } - private ContentValues getContentValues(String word1, String word2, String locale) { + private static ContentValues getContentValues(String word1, String word2, String locale) { ContentValues values = new ContentValues(3); values.put(MAIN_COLUMN_WORD1, word1); values.put(MAIN_COLUMN_WORD2, word2); @@ -388,7 +388,7 @@ public class UserBigramDictionary extends ExpandableDictionary { return values; } - private ContentValues getFrequencyContentValues(int pairId, int frequency) { + private static ContentValues getFrequencyContentValues(int pairId, int frequency) { ContentValues values = new ContentValues(2); values.put(FREQ_COLUMN_PAIR_ID, pairId); values.put(FREQ_COLUMN_FREQUENCY, frequency); diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java index 0bbbf3995..6d6296e10 100644 --- a/java/src/com/android/inputmethod/latin/UserDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserDictionary.java @@ -18,12 +18,10 @@ package com.android.inputmethod.latin; import android.content.ContentProviderClient; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; import android.provider.UserDictionary.Words; import android.text.TextUtils; @@ -38,11 +36,9 @@ public class UserDictionary extends ExpandableDictionary { Words.FREQUENCY, }; - private static final String[] PROJECTION_ADD = { - Words._ID, - Words.FREQUENCY, - Words.LOCALE, - }; + // This is not exported by the framework so we pretty much have to write it here verbatim + private static final String ACTION_USER_DICTIONARY_INSERT = + "com.android.settings.USER_DICTIONARY_INSERT"; private ContentObserver mObserver; final private String mLocale; @@ -134,7 +130,11 @@ public class UserDictionary extends ExpandableDictionary { final Cursor cursor = getContext().getContentResolver() .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null); - addWords(cursor); + try { + addWords(cursor); + } finally { + if (null != cursor) cursor.close(); + } } public boolean isEnabled() { @@ -160,54 +160,19 @@ public class UserDictionary extends ExpandableDictionary { public synchronized void addWord(final String word, final int frequency) { // Force load the dictionary here synchronously if (getRequiresReload()) loadDictionaryAsync(); + // TODO: do something for the UI. With the following, any sufficiently long word will + // look like it will go to the user dictionary but it won't. // Safeguard against adding long words. Can cause stack overflow. if (word.length() >= getMaxWordLength()) return; super.addWord(word, frequency); - // Update the user dictionary provider - final ContentValues values = new ContentValues(5); - values.put(Words.WORD, word); - values.put(Words.FREQUENCY, frequency); - values.put(Words.LOCALE, mLocale); - values.put(Words.APP_ID, 0); - - final ContentResolver contentResolver = getContext().getContentResolver(); - final ContentProviderClient client = - contentResolver.acquireContentProviderClient(Words.CONTENT_URI); - if (null == client) return; - new Thread("addWord") { - @Override - public void run() { - Cursor cursor = null; - try { - cursor = client.query(Words.CONTENT_URI, PROJECTION_ADD, - "word=? and ((locale IS NULL) or (locale=?))", - new String[] { word, mLocale }, null); - if (cursor != null && cursor.moveToFirst()) { - final String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE)); - // If locale is null, we will not override the entry. - if (locale != null && locale.equals(mLocale.toString())) { - final long id = cursor.getLong(cursor.getColumnIndex(Words._ID)); - final Uri uri = - Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id)); - // Update the entry with new frequency value. - client.update(uri, values, null, null); - } - } else { - // Insert new entry. - client.insert(Words.CONTENT_URI, values); - } - } catch (RemoteException e) { - // If we come here, the activity is already about to be killed, and we - // have no means of contacting the content provider any more. - // See ContentResolver#insert, inside the catch(){} - } finally { - if (null != cursor) cursor.close(); - client.release(); - } - } - }.start(); + // TODO: Add an argument to the intent to specify the frequency. + Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT); + intent.putExtra(Words.WORD, word); + intent.putExtra(Words.LOCALE, mLocale); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); // In case the above does a synchronous callback of the change observer setRequiresReload(false); @@ -242,6 +207,5 @@ public class UserDictionary extends ExpandableDictionary { cursor.moveToNext(); } } - cursor.close(); } } diff --git a/java/src/com/android/inputmethod/latin/UserUnigramDictionary.java b/java/src/com/android/inputmethod/latin/UserUnigramDictionary.java index e41230b3c..a7f57ae46 100644 --- a/java/src/com/android/inputmethod/latin/UserUnigramDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserUnigramDictionary.java @@ -149,7 +149,7 @@ public class UserUnigramDictionary extends ExpandableDictionary { final int length = word.length(); // Don't add very short or very long words. if (length < 2 || length > getMaxWordLength()) return; - if (mIme.getCurrentWord().isAutoCapitalized()) { + if (mIme.isAutoCapitalized()) { // Remove caps before adding word = Character.toLowerCase(word.charAt(0)) + word.substring(1); } @@ -172,7 +172,7 @@ public class UserUnigramDictionary extends ExpandableDictionary { // Nothing pending? Return if (mPendingWrites.isEmpty()) return; // Create a background thread to write the pending entries - new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute(); + new UpdateDbTask(sOpenHelper, mPendingWrites, mLocale).execute(); // Create a new map for writing new entries into while the old one is written to db mPendingWrites = new HashMap<String, Integer>(); } @@ -206,7 +206,7 @@ public class UserUnigramDictionary extends ExpandableDictionary { } } - private Cursor query(String selection, String[] selectionArgs) { + private static Cursor query(String selection, String[] selectionArgs) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(USER_UNIGRAM_DICT_TABLE_NAME); qb.setProjectionMap(sDictProjectionMap); @@ -227,8 +227,8 @@ public class UserUnigramDictionary extends ExpandableDictionary { private final DatabaseHelper mDbHelper; private final String mLocale; - public UpdateDbTask(@SuppressWarnings("unused") Context context, DatabaseHelper openHelper, - HashMap<String, Integer> pendingWrites, String locale) { + public UpdateDbTask(DatabaseHelper openHelper, HashMap<String, Integer> pendingWrites, + String locale) { mMap = pendingWrites; mLocale = locale; mDbHelper = openHelper; @@ -251,7 +251,7 @@ public class UserUnigramDictionary extends ExpandableDictionary { return null; } - private ContentValues getContentValues(String word, int frequency, String locale) { + private static ContentValues getContentValues(String word, int frequency, String locale) { ContentValues values = new ContentValues(4); values.put(COLUMN_WORD, word); values.put(COLUMN_FREQUENCY, frequency); diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index b29ff1975..38148438b 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -16,12 +16,22 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; +import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; +import com.android.inputmethod.compat.InputTypeCompatUtils; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.latin.define.JniLibName; + import android.content.Context; -import android.content.SharedPreferences; +import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; +import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; +import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; @@ -31,20 +41,15 @@ import android.text.format.DateUtils; import android.util.Log; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; -import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; -import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; -import com.android.inputmethod.compat.InputTypeCompatUtils; -import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardId; - import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; +import java.nio.channels.FileChannel; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -113,8 +118,9 @@ public class Utils { } public static boolean hasMultipleEnabledIMEsOrSubtypes( - final InputMethodManagerCompatWrapper imm, final boolean shouldIncludeAuxiliarySubtypes) { + final InputMethodManagerCompatWrapper imm = InputMethodManagerCompatWrapper.getInstance(); + if (imm == null) return false; final List<InputMethodInfoCompatWrapper> enabledImis = imm.getEnabledInputMethodList(); // Number of the filtered IMEs @@ -185,7 +191,8 @@ public class Utils { final int typedWordLength = typedWord.length(); final int maxEditDistanceOfNativeDictionary = (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1; - final int distance = Utils.editDistance(typedWord, suggestionWord); + final int distance = BinaryDictionary.editDistance( + typedWord.toString(), suggestionWord.toString()); if (DBG) { Log.d(TAG, "Autocorrected edit distance = " + distance + ", " + maxEditDistanceOfNativeDictionary); @@ -242,7 +249,7 @@ public class Utils { UsabilityStudyLogUtils.getInstance().init(context); return sRingCharBuffer; } - private int normalize(int in) { + private static int normalize(int in) { int ret = in % BUFSIZE; return ret < 0 ? ret + BUFSIZE : ret; } @@ -317,49 +324,6 @@ public class Utils { } } - - /* Damerau-Levenshtein distance */ - public static int editDistance(CharSequence s, CharSequence t) { - if (s == null || t == null) { - throw new IllegalArgumentException("editDistance: Arguments should not be null."); - } - final int sl = s.length(); - final int tl = t.length(); - int[][] dp = new int [sl + 1][tl + 1]; - for (int i = 0; i <= sl; i++) { - dp[i][0] = i; - } - for (int j = 0; j <= tl; j++) { - dp[0][j] = j; - } - for (int i = 0; i < sl; ++i) { - for (int j = 0; j < tl; ++j) { - final char sc = Character.toLowerCase(s.charAt(i)); - final char tc = Character.toLowerCase(t.charAt(j)); - final int cost = sc == tc ? 0 : 1; - dp[i + 1][j + 1] = Math.min( - dp[i][j + 1] + 1, Math.min(dp[i + 1][j] + 1, dp[i][j] + cost)); - // Overwrite for transposition cases - if (i > 0 && j > 0 - && sc == Character.toLowerCase(t.charAt(j - 1)) - && tc == Character.toLowerCase(s.charAt(i - 1))) { - dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i - 1][j - 1] + cost); - } - } - } - if (DBG_EDIT_DISTANCE) { - Log.d(TAG, "editDistance:" + s + "," + t); - for (int i = 0; i < dp.length; ++i) { - StringBuffer sb = new StringBuffer(); - for (int j = 0; j < dp[i].length; ++j) { - sb.append(dp[i][j]).append(','); - } - Log.d(TAG, i + ":" + sb.toString()); - } - } - return dp[sl][tl]; - } - // Get the current stack trace public static String getStackTrace() { StringBuilder sb = new StringBuilder(); @@ -373,55 +337,6 @@ public class Utils { return sb.toString(); } - // In dictionary.cpp, getSuggestion() method, - // suggestion scores are computed using the below formula. - // original score - // := pow(mTypedLetterMultiplier (this is defined 2), - // (the number of matched characters between typed word and suggested word)) - // * (individual word's score which defined in the unigram dictionary, - // and this score is defined in range [0, 255].) - // Then, the following processing is applied. - // - If the dictionary word is matched up to the point of the user entry - // (full match up to min(before.length(), after.length()) - // => Then multiply by FULL_MATCHED_WORDS_PROMOTION_RATE (this is defined 1.2) - // - If the word is a true full match except for differences in accents or - // capitalization, then treat it as if the score was 255. - // - If before.length() == after.length() - // => multiply by mFullWordMultiplier (this is defined 2)) - // So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2 - // For historical reasons we ignore the 1.2 modifier (because the measure for a good - // autocorrection threshold was done at a time when it didn't exist). This doesn't change - // the result. - // So, we can normalize original score by dividing pow(2, min(b.l(),a.l())) * 255 * 2. - private static final int MAX_INITIAL_SCORE = 255; - private static final int TYPED_LETTER_MULTIPLIER = 2; - private static final int FULL_WORD_MULTIPLIER = 2; - private static final int S_INT_MAX = 2147483647; - public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) { - final int beforeLength = before.length(); - final int afterLength = after.length(); - if (beforeLength == 0 || afterLength == 0) return 0; - final int distance = editDistance(before, after); - // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character - // correction. - int spaceCount = 0; - for (int i = 0; i < afterLength; ++i) { - if (after.charAt(i) == Keyboard.CODE_SPACE) { - ++spaceCount; - } - } - if (spaceCount == afterLength) return 0; - final double maximumScore = score == S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE - * Math.pow( - TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength - spaceCount)) - * FULL_WORD_MULTIPLIER; - // add a weight based on edit distance. - // distance <= max(afterLength, beforeLength) == afterLength, - // so, 0 <= distance / afterLength <= 1 - final double weight = 1.0 - (double) distance / afterLength; - return (score / maximumScore) * weight; - } - public static class UsabilityStudyLogUtils { private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); private static final String FILENAME = "log.txt"; @@ -465,7 +380,7 @@ public class Utils { } } - public void writeBackSpace() { + public static void writeBackSpace() { UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0"); } @@ -504,32 +419,89 @@ public class Utils { }); } - public void printAll() { + private synchronized String getBufferedLogs() { + mWriter.flush(); + StringBuilder sb = new StringBuilder(); + BufferedReader br = getBufferedReader(); + String line; + try { + while ((line = br.readLine()) != null) { + sb.append('\n'); + sb.append(line); + } + } catch (IOException e) { + Log.e(USABILITY_TAG, "Can't read log file."); + } finally { + if (LatinImeLogger.sDBG) { + Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString()); + } + try { + br.close(); + } catch (IOException e) { + // ignore. + } + } + return sb.toString(); + } + + public void emailResearcherLogsAll() { mLoggingHandler.post(new Runnable() { @Override public void run() { + final Date date = new Date(); + date.setTime(System.currentTimeMillis()); + final String currentDateTimeString = + new SimpleDateFormat("yyyyMMdd-HHmmssZ").format(date); + if (mFile == null) { + Log.w(USABILITY_TAG, "No internal log file found."); + return; + } + if (mIms.checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE"); + return; + } mWriter.flush(); - StringBuilder sb = new StringBuilder(); - BufferedReader br = getBufferedReader(); - String line; + final String destPath = Environment.getExternalStorageDirectory() + + "/research-" + currentDateTimeString + ".log"; + final File destFile = new File(destPath); try { - while ((line = br.readLine()) != null) { - sb.append('\n'); - sb.append(line); - } - } catch (IOException e) { - Log.e(USABILITY_TAG, "Can't read log file."); - } finally { - if (LatinImeLogger.sDBG) { - Log.d(USABILITY_TAG, "output all logs\n" + sb.toString()); - } - mIms.getCurrentInputConnection().commitText(sb.toString(), 0); - try { - br.close(); - } catch (IOException e) { - // ignore. - } + final FileChannel src = (new FileInputStream(mFile)).getChannel(); + final FileChannel dest = (new FileOutputStream(destFile)).getChannel(); + src.transferTo(0, src.size(), dest); + src.close(); + dest.close(); + } catch (FileNotFoundException e1) { + Log.w(USABILITY_TAG, e1); + return; + } catch (IOException e2) { + Log.w(USABILITY_TAG, e2); + return; } + if (destFile == null || !destFile.exists()) { + Log.w(USABILITY_TAG, "Dest file doesn't exist."); + return; + } + final Intent intent = new Intent(Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (LatinImeLogger.sDBG) { + Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI()); + } + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath)); + intent.putExtra(Intent.EXTRA_SUBJECT, + "[Research Logs] " + currentDateTimeString); + mIms.startActivity(intent); + } + }); + } + + public void printAll() { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0); } }); } @@ -630,9 +602,13 @@ public class Utils { public static void loadNativeLibrary() { try { - System.loadLibrary("jni_latinime"); + System.loadLibrary(JniLibName.JNI_LIB_NAME); } catch (UnsatisfiedLinkError ule) { - Log.e(TAG, "Could not load native library jni_latinime"); + Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME); + if (LatinImeLogger.sDBG) { + throw new RuntimeException( + "Could not load native library " + JniLibName.JNI_LIB_NAME); + } } } @@ -778,40 +754,32 @@ public class Utils { return s.toUpperCase(locale).charAt(0) + s.substring(1); } - public static int getCurrentVibrationDuration(SharedPreferences sp, Resources res) { - final int ms = sp.getInt(Settings.PREF_KEYPRESS_VIBRATION_DURATION_SETTINGS, -1); - if (ms >= 0) { - return ms; - } - final String[] durationPerHardwareList = res.getStringArray( - R.array.keypress_vibration_durations); - final String hardwarePrefix = Build.HARDWARE + ","; - for (final String element : durationPerHardwareList) { - if (element.startsWith(hardwarePrefix)) { - return (int)Long.parseLong(element.substring(element.lastIndexOf(',') + 1)); - } - } - return -1; + public static boolean willAutoCorrect(SuggestedWords suggestions) { + return !suggestions.mTypedWordValid && suggestions.mHasAutoCorrectionCandidate + && !suggestions.shouldBlockAutoCorrection(); } - public static float getCurrentKeypressSoundVolume(SharedPreferences sp, Resources res) { - final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); - if (volume >= 0) { - return volume; + public static class Stats { + public static void onNonSeparator(final char code, final int x, + final int y) { + RingCharBuffer.getInstance().push(code, x, y); + LatinImeLogger.logOnInputChar(); } - final String[] volumePerHardwareList = res.getStringArray(R.array.keypress_volumes); - final String hardwarePrefix = Build.HARDWARE + ","; - for (final String element : volumePerHardwareList) { - if (element.startsWith(hardwarePrefix)) { - return Float.parseFloat(element.substring(element.lastIndexOf(',') + 1)); - } + public static void onSeparator(final char code, final int x, + final int y) { + RingCharBuffer.getInstance().push(code, x, y); + LatinImeLogger.logOnInputSeparator(); } - return -1.0f; - } - public static boolean willAutoCorrect(SuggestedWords suggestions) { - return !suggestions.mTypedWordValid && suggestions.mHasAutoCorrectionCandidate - && !suggestions.shouldBlockAutoCorrection(); + public static void onAutoCorrection(final String typedWord, final String correctedWord, + final int separatorCode) { + if (TextUtils.isEmpty(typedWord)) return; + LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode); + } + + public static void onAutoCorrectionCancellation() { + LatinImeLogger.logOnAutoCorrectionCancelled(); + } } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index adc5637f6..e95dcfdc9 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2008 The Android Open Source Project - * + * * 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,9 +16,14 @@ package com.android.inputmethod.latin; +import android.text.TextUtils; + +import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.Keyboard; import java.util.ArrayList; +import java.util.Arrays; /** * A place to store the currently composing word with information such as adjacent key codes as well @@ -28,31 +33,75 @@ public class WordComposer { public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE; public static final int NOT_A_COORDINATE = -1; - /** - * The list of unicode values for each keystroke (including surrounding keys) - */ - private ArrayList<int[]> mCodes; + // TODO: Straighten out commit behavior so that the flags here are more understandable, + // and possibly adjust their names. + // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with + // no hinting from the IME. It happens when some external event happens (rotating the device, + // for example) or when auto-correction is off by settings or editor attributes. + public static final int COMMIT_TYPE_USER_TYPED_WORD = 0; + // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip. + public static final int COMMIT_TYPE_MANUAL_PICK = 1; + // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best + // for the current user input. It may be different from what the user typed (true auto-correct) + // or it may be exactly what the user typed if it's in the dictionary or the IME does not have + // enough confidence in any suggestion to auto-correct (auto-correct to typed word). + public static final int COMMIT_TYPE_DECIDED_WORD = 2; + // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling + // an auto-correction. + public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3; - private int[] mXCoordinates; - private int[] mYCoordinates; + // Storage for all the info about the current input. + private static class CharacterStore { + /** + * The list of unicode values for each keystroke (including surrounding keys) + */ + ArrayList<int[]> mCodes; + int[] mXCoordinates; + int[] mYCoordinates; + StringBuilder mTypedWord; + CharSequence mAutoCorrection; + CharacterStore() { + final int N = BinaryDictionary.MAX_WORD_LENGTH; + mCodes = new ArrayList<int[]>(N); + mTypedWord = new StringBuilder(N); + mXCoordinates = new int[N]; + mYCoordinates = new int[N]; + mAutoCorrection = null; + } + CharacterStore(final CharacterStore that) { + mCodes = new ArrayList<int[]>(that.mCodes); + mTypedWord = new StringBuilder(that.mTypedWord); + mXCoordinates = Arrays.copyOf(that.mXCoordinates, that.mXCoordinates.length); + mYCoordinates = Arrays.copyOf(that.mYCoordinates, that.mYCoordinates.length); + } + void reset() { + // For better performance than creating a new character store. + mCodes.clear(); + mTypedWord.setLength(0); + mAutoCorrection = null; + } + } - private StringBuilder mTypedWord; + // The currently typing word. May not be null. + private CharacterStore mCurrentWord; + // The information being kept for resuming suggestion. May be null if wiped. + private CharacterStore mCommittedWordSavedForSuggestionResuming; private int mCapsCount; private boolean mAutoCapitalized; - + // Cache this value for performance + private int mTrailingSingleQuotesCount; + /** * Whether the user chose to capitalize the first char of the word. */ private boolean mIsFirstCharCapitalized; public WordComposer() { - final int N = BinaryDictionary.MAX_WORD_LENGTH; - mCodes = new ArrayList<int[]>(N); - mTypedWord = new StringBuilder(N); - mXCoordinates = new int[N]; - mYCoordinates = new int[N]; + mCurrentWord = new CharacterStore(); + mCommittedWordSavedForSuggestionResuming = null; + mTrailingSingleQuotesCount = 0; } public WordComposer(WordComposer source) { @@ -60,23 +109,23 @@ public class WordComposer { } public void init(WordComposer source) { - mCodes = new ArrayList<int[]>(source.mCodes); - mTypedWord = new StringBuilder(source.mTypedWord); - mXCoordinates = source.mXCoordinates; - mYCoordinates = source.mYCoordinates; + mCurrentWord = new CharacterStore(source.mCurrentWord); + mCommittedWordSavedForSuggestionResuming = source.mCommittedWordSavedForSuggestionResuming; mCapsCount = source.mCapsCount; mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; mAutoCapitalized = source.mAutoCapitalized; + mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; } /** * Clear out the keys registered so far. */ public void reset() { - mCodes.clear(); - mTypedWord.setLength(0); + mCurrentWord.reset(); + mCommittedWordSavedForSuggestionResuming = null; mCapsCount = 0; mIsFirstCharCapitalized = false; + mTrailingSingleQuotesCount = 0; } /** @@ -84,7 +133,11 @@ public class WordComposer { * @return the number of keystrokes */ public final int size() { - return mTypedWord.length(); + return mCurrentWord.mTypedWord.length(); + } + + public final boolean isComposingWord() { + return size() > 0; } /** @@ -93,15 +146,15 @@ public class WordComposer { * @return the unicode for the pressed and surrounding keys */ public int[] getCodesAt(int index) { - return mCodes.get(index); + return mCurrentWord.mCodes.get(index); } public int[] getXCoordinates() { - return mXCoordinates; + return mCurrentWord.mXCoordinates; } public int[] getYCoordinates() { - return mYCoordinates; + return mCurrentWord.mYCoordinates; } private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) { @@ -116,16 +169,67 @@ public class WordComposer { */ public void add(int primaryCode, int[] codes, int x, int y) { final int newIndex = size(); - mTypedWord.append((char) primaryCode); + mCurrentWord.mTypedWord.append((char) primaryCode); correctPrimaryJuxtapos(primaryCode, codes); - mCodes.add(codes); + mCurrentWord.mCodes.add(codes); if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { - mXCoordinates[newIndex] = x; - mYCoordinates[newIndex] = y; + mCurrentWord.mXCoordinates[newIndex] = x; + mCurrentWord.mYCoordinates[newIndex] = y; } mIsFirstCharCapitalized = isFirstCharCapitalized( newIndex, primaryCode, mIsFirstCharCapitalized); if (Character.isUpperCase(primaryCode)) mCapsCount++; + if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { + ++mTrailingSingleQuotesCount; + } else { + mTrailingSingleQuotesCount = 0; + } + mCurrentWord.mAutoCorrection = null; + } + + /** + * Internal method to retrieve reasonable proximity info for a character. + */ + private void addKeyInfo(final int codePoint, final Keyboard keyboard, + final KeyDetector keyDetector) { + for (final Key key : keyboard.mKeys) { + if (key.mCode == codePoint) { + final int x = key.mX + key.mWidth / 2; + final int y = key.mY + key.mHeight / 2; + final int[] codes = keyDetector.newCodeArray(); + keyDetector.getKeyAndNearbyCodes(x, y, codes); + add(codePoint, codes, x, y); + return; + } + } + add(codePoint, new int[] { codePoint }, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); + } + + /** + * Set the currently composing word to the one passed as an argument. + * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. + */ + public void setComposingWord(final CharSequence word, final Keyboard keyboard, + final KeyDetector keyDetector) { + reset(); + final int length = word.length(); + for (int i = 0; i < length; ++i) { + int codePoint = word.charAt(i); + addKeyInfo(codePoint, keyboard, keyDetector); + } + mCommittedWordSavedForSuggestionResuming = null; + } + + /** + * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard. + */ + public void setComposingWord(final CharSequence word, final Keyboard keyboard) { + final KeyDetector keyDetector = new KeyDetector(0); + keyDetector.setKeyboard(keyboard, 0, 0); + keyDetector.setProximityCorrectionEnabled(true); + keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth); + setComposingWord(word, keyboard, keyDetector); } /** @@ -135,7 +239,7 @@ public class WordComposer { * @param primaryCode the preferred character * @param codes array of codes based on distance from touch point */ - private void correctPrimaryJuxtapos(int primaryCode, int[] codes) { + private static void correctPrimaryJuxtapos(int primaryCode, int[] codes) { if (codes.length < 2) return; if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) { codes[1] = codes[0]; @@ -150,25 +254,31 @@ public class WordComposer { final int size = size(); if (size > 0) { final int lastPos = size - 1; - char lastChar = mTypedWord.charAt(lastPos); - mCodes.remove(lastPos); - mTypedWord.deleteCharAt(lastPos); + char lastChar = mCurrentWord.mTypedWord.charAt(lastPos); + mCurrentWord.mCodes.remove(lastPos); + mCurrentWord.mTypedWord.deleteCharAt(lastPos); if (Character.isUpperCase(lastChar)) mCapsCount--; } if (size() == 0) { mIsFirstCharCapitalized = false; } + if (mTrailingSingleQuotesCount > 0) { + --mTrailingSingleQuotesCount; + } else { + for (int i = mCurrentWord.mTypedWord.length() - 1; i >= 0; --i) { + if (Keyboard.CODE_SINGLE_QUOTE != mCurrentWord.mTypedWord.codePointAt(i)) break; + ++mTrailingSingleQuotesCount; + } + } + mCurrentWord.mAutoCorrection = null; } /** * Returns the word as it was typed, without any correction applied. - * @return the word that was typed so far + * @return the word that was typed so far. Never returns null. */ public String getTypedWord() { - if (size() == 0) { - return null; - } - return mTypedWord.toString(); + return mCurrentWord.mTypedWord.toString(); } /** @@ -179,6 +289,10 @@ public class WordComposer { return mIsFirstCharCapitalized; } + public int trailingSingleQuotesCount() { + return mTrailingSingleQuotesCount; + } + /** * Whether or not all of the user typed chars are upper case * @return true if all user typed chars are upper case, false otherwise @@ -194,7 +308,7 @@ public class WordComposer { 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 @@ -210,4 +324,62 @@ public class WordComposer { public boolean isAutoCapitalized() { return mAutoCapitalized; } + + /** + * Sets the auto-correction for this word. + */ + public void setAutoCorrection(final CharSequence correction) { + mCurrentWord.mAutoCorrection = correction; + } + + /** + * Remove any auto-correction that may have been set. + */ + public void deleteAutoCorrection() { + mCurrentWord.mAutoCorrection = null; + } + + /** + * @return the auto-correction for this word, or null if none. + */ + public CharSequence getAutoCorrectionOrNull() { + return mCurrentWord.mAutoCorrection; + } + + // `type' should be one of the COMMIT_TYPE_* constants above. + public void onCommitWord(final int type) { + mCommittedWordSavedForSuggestionResuming = mCurrentWord; + // Note: currently, we come here whenever we commit a word. If it's any *other* kind than + // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later. + // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user + // typed because the IME decided *not* to auto-correct for whatever reason. + // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct. + // As it happens these two cases should behave differently, because the former can be + // canceled while the latter can't. Currently, we figure this out in + // #didAutoCorrectToAnotherWord with #equals(). It would be marginally cleaner to do it + // here, but it would be slower (since we would #equals() for each commit, instead of + // only on cancel), and ultimately we want to figure it out even earlier anyway. + if (type != COMMIT_TYPE_DECIDED_WORD) { + // Only ever revert an auto-correct. + mCommittedWordSavedForSuggestionResuming.mAutoCorrection = null; + } + // TODO: improve performance by swapping buffers instead of creating a new object. + mCurrentWord = new CharacterStore(); + } + + public boolean hasWordKeptForSuggestionResuming() { + return null != mCommittedWordSavedForSuggestionResuming; + } + + public void resumeSuggestionOnKeptWord() { + mCurrentWord = mCommittedWordSavedForSuggestionResuming; + mCommittedWordSavedForSuggestionResuming = null; + } + + public boolean didAutoCorrectToAnotherWord() { + return null != mCommittedWordSavedForSuggestionResuming + && !TextUtils.isEmpty(mCommittedWordSavedForSuggestionResuming.mAutoCorrection) + && !TextUtils.equals(mCommittedWordSavedForSuggestionResuming.mTypedWord, + mCommittedWordSavedForSuggestionResuming.mAutoCorrection); + } } diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java new file mode 100644 index 000000000..d747a024c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 android.content.res.TypedArray; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +public class XmlParseUtils { + @SuppressWarnings("serial") + public static class ParseException extends XmlPullParserException { + public ParseException(String msg, XmlPullParser parser) { + super(msg + " at line " + parser.getLineNumber() + + ", column " + parser.getColumnNumber()); + } + } + + @SuppressWarnings("serial") + public static class IllegalStartTag extends ParseException { + public IllegalStartTag(XmlPullParser parser, String parent) { + super("Illegal start tag " + parser.getName() + " in " + parent, parser); + } + } + + @SuppressWarnings("serial") + public static class IllegalEndTag extends ParseException { + public IllegalEndTag(XmlPullParser parser, String parent) { + super("Illegal end tag " + parser.getName() + " in " + parent, parser); + } + } + + @SuppressWarnings("serial") + public static class IllegalAttribute extends ParseException { + public IllegalAttribute(XmlPullParser parser, String attribute) { + super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser); + } + } + + @SuppressWarnings("serial") + public static class NonEmptyTag extends ParseException{ + public NonEmptyTag(String tag, XmlPullParser parser) { + super(tag + " must be empty tag", parser); + } + } + + public static void checkEndTag(String tag, XmlPullParser parser) + throws XmlPullParserException, IOException { + if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName())) + return; + throw new NonEmptyTag(tag, parser); + } + + public static void checkAttributeExists(TypedArray attr, int attrId, String attrName, + String tag, XmlPullParser parser) throws XmlPullParserException { + if (attr.hasValue(attrId)) + return; + throw new ParseException( + "No " + attrName + " attribute found in <" + tag + "/>", parser); + } +} diff --git a/java/src/com/android/inputmethod/latin/define/JniLibName.java b/java/src/com/android/inputmethod/latin/define/JniLibName.java new file mode 100644 index 000000000..3e94a3c07 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/define/JniLibName.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.define; + +public class JniLibName { + public static final String JNI_LIB_NAME = "jni_latinime"; +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 095c2c51c..39e47f661 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -17,7 +17,9 @@ package com.android.inputmethod.latin.spellcheck; import android.content.Intent; +import android.content.SharedPreferences; import android.content.res.Resources; +import android.preference.PreferenceManager; import android.service.textservice.SpellCheckerService; import android.text.TextUtils; import android.util.Log; @@ -25,6 +27,7 @@ import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import com.android.inputmethod.compat.ArraysCompatUtils; +import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.BinaryDictionary; import com.android.inputmethod.latin.Dictionary; @@ -41,21 +44,27 @@ import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.WhitelistDictionary; import com.android.inputmethod.latin.WordComposer; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.TreeMap; +import java.util.HashSet; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. */ -public class AndroidSpellCheckerService extends SpellCheckerService { +public class AndroidSpellCheckerService extends SpellCheckerService + implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); private static final boolean DBG = false; private static final int POOL_SIZE = 2; + public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; + private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case private static final int CAPITALIZE_FIRST = 1; // First only private static final int CAPITALIZE_ALL = 2; // All caps @@ -82,15 +91,100 @@ public class AndroidSpellCheckerService extends SpellCheckerService { // The threshold for a candidate to be offered as a suggestion. private double mSuggestionThreshold; - // The threshold for a suggestion to be considered "likely". - private double mLikelyThreshold; + // The threshold for a suggestion to be considered "recommended". + private double mRecommendedThreshold; + // Whether to use the contacts dictionary + private boolean mUseContactsDictionary; + private final Object mUseContactsLock = new Object(); + + private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = + new HashSet<WeakReference<DictionaryCollection>>(); + + public static final int SCRIPT_LATIN = 0; + public static final int SCRIPT_CYRILLIC = 1; + private static final TreeMap<String, Integer> mLanguageToScript; + static { + // List of the supported languages and their associated script. We won't check + // words written in another script than the selected script, because we know we + // don't have those in our dictionary so we will underline everything and we + // will never have any suggestions, so it makes no sense checking them. + mLanguageToScript = new TreeMap<String, Integer>(); + mLanguageToScript.put("en", SCRIPT_LATIN); + mLanguageToScript.put("fr", SCRIPT_LATIN); + mLanguageToScript.put("de", SCRIPT_LATIN); + mLanguageToScript.put("nl", SCRIPT_LATIN); + mLanguageToScript.put("cs", SCRIPT_LATIN); + mLanguageToScript.put("es", SCRIPT_LATIN); + mLanguageToScript.put("it", SCRIPT_LATIN); + mLanguageToScript.put("ru", SCRIPT_CYRILLIC); + } @Override public void onCreate() { super.onCreate(); mSuggestionThreshold = Double.parseDouble(getString(R.string.spellchecker_suggestion_threshold_value)); - mLikelyThreshold = - Double.parseDouble(getString(R.string.spellchecker_likely_threshold_value)); + mRecommendedThreshold = + Double.parseDouble(getString(R.string.spellchecker_recommended_threshold_value)); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); + } + + private static int getScriptFromLocale(final Locale locale) { + final Integer script = mLanguageToScript.get(locale.getLanguage()); + if (null == script) { + throw new RuntimeException("We have been called with an unsupported language: \"" + + locale.getLanguage() + "\". Framework bug?"); + } + return script; + } + + @Override + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { + if (!PREF_USE_CONTACTS_KEY.equals(key)) return; + synchronized(mUseContactsLock) { + mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); + if (mUseContactsDictionary) { + startUsingContactsDictionaryLocked(); + } else { + stopUsingContactsDictionaryLocked(); + } + } + } + + private void startUsingContactsDictionaryLocked() { + if (null == mContactsDictionary) { + mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this); + } + final Iterator<WeakReference<DictionaryCollection>> iterator = + mDictionaryCollectionsList.iterator(); + while (iterator.hasNext()) { + final WeakReference<DictionaryCollection> dictRef = iterator.next(); + final DictionaryCollection dict = dictRef.get(); + if (null == dict) { + iterator.remove(); + } else { + dict.addDictionary(mContactsDictionary); + } + } + } + + private void stopUsingContactsDictionaryLocked() { + if (null == mContactsDictionary) return; + final SynchronouslyLoadedContactsDictionary contactsDict = mContactsDictionary; + mContactsDictionary = null; + final Iterator<WeakReference<DictionaryCollection>> iterator = + mDictionaryCollectionsList.iterator(); + while (iterator.hasNext()) { + final WeakReference<DictionaryCollection> dictRef = iterator.next(); + final DictionaryCollection dict = dictRef.get(); + if (null == dict) { + iterator.remove(); + } else { + dict.removeDictionary(contactsDict); + } + } + contactsDict.close(); } @Override @@ -110,10 +204,11 @@ public class AndroidSpellCheckerService extends SpellCheckerService { private static class SuggestionsGatherer implements WordCallback { public static class Result { public final String[] mSuggestions; - public final boolean mHasLikelySuggestions; - public Result(final String[] gatheredSuggestions, final boolean hasLikelySuggestions) { + public final boolean mHasRecommendedSuggestions; + public Result(final String[] gatheredSuggestions, + final boolean hasRecommendedSuggestions) { mSuggestions = gatheredSuggestions; - mHasLikelySuggestions = hasLikelySuggestions; + mHasRecommendedSuggestions = hasRecommendedSuggestions; } } @@ -121,7 +216,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { private final int[] mScores; private final String mOriginalText; private final double mSuggestionThreshold; - private final double mLikelyThreshold; + private final double mRecommendedThreshold; private final int mMaxLength; private int mLength = 0; @@ -131,10 +226,10 @@ public class AndroidSpellCheckerService extends SpellCheckerService { private int mBestScore = Integer.MIN_VALUE; // As small as possible SuggestionsGatherer(final String originalText, final double suggestionThreshold, - final double likelyThreshold, final int maxLength) { + final double recommendedThreshold, final int maxLength) { mOriginalText = originalText; mSuggestionThreshold = suggestionThreshold; - mLikelyThreshold = likelyThreshold; + mRecommendedThreshold = recommendedThreshold; mMaxLength = maxLength; mSuggestions = new ArrayList<CharSequence>(maxLength + 1); mScores = new int[mMaxLength]; @@ -175,7 +270,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { // make the threshold. final String wordString = new String(word, wordOffset, wordLength); final double normalizedScore = - Utils.calcNormalizedScore(mOriginalText, wordString, score); + BinaryDictionary.calcNormalizedScore(mOriginalText, wordString, score); if (normalizedScore < mSuggestionThreshold) { if (DBG) Log.i(TAG, wordString + " does not make the score threshold"); return true; @@ -198,19 +293,19 @@ public class AndroidSpellCheckerService extends SpellCheckerService { public Result getResults(final int capitalizeType, final Locale locale) { final String[] gatheredSuggestions; - final boolean hasLikelySuggestions; + final boolean hasRecommendedSuggestions; if (0 == mLength) { // Either we found no suggestions, or we found some BUT the max length was 0. // If we found some mBestSuggestion will not be null. If it is null, then // we found none, regardless of the max length. if (null == mBestSuggestion) { gatheredSuggestions = null; - hasLikelySuggestions = false; + hasRecommendedSuggestions = false; } else { gatheredSuggestions = EMPTY_STRING_ARRAY; - final double normalizedScore = - Utils.calcNormalizedScore(mOriginalText, mBestSuggestion, mBestScore); - hasLikelySuggestions = (normalizedScore > mLikelyThreshold); + final double normalizedScore = BinaryDictionary.calcNormalizedScore( + mOriginalText, mBestSuggestion, mBestScore); + hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); } } else { if (DBG) { @@ -243,16 +338,17 @@ public class AndroidSpellCheckerService extends SpellCheckerService { final int bestScore = mScores[mLength - 1]; final CharSequence bestSuggestion = mSuggestions.get(0); final double normalizedScore = - Utils.calcNormalizedScore(mOriginalText, bestSuggestion, bestScore); - hasLikelySuggestions = (normalizedScore > mLikelyThreshold); + BinaryDictionary.calcNormalizedScore( + mOriginalText, bestSuggestion.toString(), bestScore); + hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); if (DBG) { Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); Log.i(TAG, "Normalized score = " + normalizedScore - + " (threshold " + mLikelyThreshold - + ") => hasLikelySuggestions = " + hasLikelySuggestions); + + " (threshold " + mRecommendedThreshold + + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions); } } - return new Result(gatheredSuggestions, hasLikelySuggestions); + return new Result(gatheredSuggestions, hasRecommendedSuggestions); } } @@ -273,13 +369,15 @@ public class AndroidSpellCheckerService extends SpellCheckerService { for (Dictionary dict : oldWhitelistDictionaries.values()) { dict.close(); } - if (null != mContactsDictionary) { - // The synchronously loaded contacts dictionary should have been in one - // or several pools, but it is shielded against multiple closing and it's - // safe to call it several times. - final SynchronouslyLoadedContactsDictionary dictToClose = mContactsDictionary; - mContactsDictionary = null; - dictToClose.close(); + synchronized(mUseContactsLock) { + if (null != mContactsDictionary) { + // The synchronously loaded contacts dictionary should have been in one + // or several pools, but it is shielded against multiple closing and it's + // safe to call it several times. + final SynchronouslyLoadedContactsDictionary dictToClose = mContactsDictionary; + mContactsDictionary = null; + dictToClose.close(); + } } return false; } @@ -295,7 +393,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService { } public DictAndProximity createDictAndProximity(final Locale locale) { - final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(); + final int script = getScriptFromLocale(locale); + final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo( + SpellCheckerProximityInfo.getProximityForScript(script)); final Resources resources = getResources(); final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); final DictionaryCollection dictionaryCollection = @@ -314,11 +414,16 @@ public class AndroidSpellCheckerService extends SpellCheckerService { mWhitelistDictionaries.put(localeStr, whitelistDictionary); } dictionaryCollection.addDictionary(whitelistDictionary); - if (null == mContactsDictionary) { - mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this); + synchronized(mUseContactsLock) { + if (mUseContactsDictionary) { + if (null == mContactsDictionary) { + mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this); + } + } + dictionaryCollection.addDictionary(mContactsDictionary); + mDictionaryCollectionsList.add( + new WeakReference<DictionaryCollection>(dictionaryCollection)); } - // TODO: add a setting to use or not contacts when checking spelling - dictionaryCollection.addDictionary(mContactsDictionary); return new DictAndProximity(dictionaryCollection, proximityInfo); } @@ -346,6 +451,8 @@ public class AndroidSpellCheckerService extends SpellCheckerService { private DictionaryPool mDictionaryPool; // Likewise private Locale mLocale; + // Cache this for performance + private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. private final AndroidSpellCheckerService mService; @@ -358,17 +465,51 @@ public class AndroidSpellCheckerService extends SpellCheckerService { final String localeString = getLocale(); mDictionaryPool = mService.getDictionaryPool(localeString); mLocale = LocaleUtils.constructLocaleFromString(localeString); + mScript = getScriptFromLocale(mLocale); + } + + /* + * Returns whether the code point is a letter that makes sense for the specified + * locale for this spell checker. + * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml + * and is limited to EFIGS languages and Russian. + * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters + * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters. + */ + private static boolean isLetterCheckableByLanguage(final int codePoint, + final int script) { + switch (script) { + case SCRIPT_LATIN: + // Our supported latin script dictionaries (EFIGS) at the moment only include + // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode + // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF, + // so the below is a very efficient way to test for it. As for the 0-0x3F, it's + // excluded from isLetter anyway. + return codePoint <= 0x2AF && Character.isLetter(codePoint); + case SCRIPT_CYRILLIC: + // All Cyrillic characters are in the 400~52F block. There are some in the upper + // Unicode range, but they are archaic characters that are not used in modern + // russian and are not used by our dictionary. + return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint); + default: + // Should never come here + throw new RuntimeException("Impossible value of script: " + script); + } } /** * Finds out whether a particular string should be filtered out of spell checking. * - * This will loosely match URLs, numbers, symbols. + * This will loosely match URLs, numbers, symbols. To avoid always underlining words that + * we know we will never recognize, this accepts a script identifier that should be one + * of the SCRIPT_* constants defined above, to rule out quickly characters from very + * different languages. * * @param text the string to evaluate. + * @param script the identifier for the script this spell checker recognizes * @return true if we should filter this text out, false otherwise */ - private boolean shouldFilterOut(final String text) { + private static boolean shouldFilterOut(final String text, final int script) { if (TextUtils.isEmpty(text) || text.length() <= 1) return true; // TODO: check if an equivalent processing can't be done more quickly with a @@ -376,7 +517,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { // Filter by first letter final int firstCodePoint = text.codePointAt(0); // Filter out words that don't start with a letter or an apostrophe - if (!Character.isLetter(firstCodePoint) + if (!isLetterCheckableByLanguage(firstCodePoint, script) && '\'' != firstCodePoint) return true; // Filter contents @@ -389,7 +530,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { // words or a URI - in either case we don't want to spell check that if ('@' == codePoint || '/' == codePoint) return true; - if (Character.isLetter(codePoint)) ++letterCount; + if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; } // Guestimate heuristic: perform spell checking if at least 3/4 of the characters // in this word are letters @@ -408,7 +549,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { try { final String text = textInfo.getText(); - if (shouldFilterOut(text)) { + if (shouldFilterOut(text, mScript)) { DictAndProximity dictInfo = null; try { dictInfo = mDictionaryPool.takeOrGetNull(); @@ -426,17 +567,23 @@ public class AndroidSpellCheckerService extends SpellCheckerService { // TODO: Don't gather suggestions if the limit is <= 0 unless necessary final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text, - mService.mSuggestionThreshold, mService.mLikelyThreshold, suggestionsLimit); + mService.mSuggestionThreshold, mService.mRecommendedThreshold, + suggestionsLimit); final WordComposer composer = new WordComposer(); final int length = text.length(); for (int i = 0; i < length; ++i) { final int character = text.codePointAt(i); - final int proximityIndex = SpellCheckerProximityInfo.getIndexOf(character); + final int proximityIndex = + SpellCheckerProximityInfo.getIndexOfCodeForScript(character, mScript); final int[] proximities; if (-1 == proximityIndex) { proximities = new int[] { character }; } else { - proximities = Arrays.copyOfRange(SpellCheckerProximityInfo.PROXIMITY, + // TODO: an initial examination seems to reveal this is actually used + // read-only. It should be possible to compute the arrays statically once + // and skip doing a copy each time here. + proximities = Arrays.copyOfRange( + SpellCheckerProximityInfo.getProximityForScript(mScript), proximityIndex, proximityIndex + SpellCheckerProximityInfo.ROW_SIZE); } @@ -475,7 +622,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService { + suggestionsLimit); Log.i(TAG, "IsInDict = " + isInDict); Log.i(TAG, "LooksLikeTypo = " + (!isInDict)); - Log.i(TAG, "HasLikelySuggestions = " + result.mHasLikelySuggestions); + Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions); if (null != result.mSuggestions) { for (String suggestion : result.mSuggestions) { Log.i(TAG, suggestion); @@ -483,10 +630,13 @@ public class AndroidSpellCheckerService extends SpellCheckerService { } } - // TODO: actually use result.mHasLikelySuggestions final int flags = (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY - : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO); + : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) + | (result.mHasRecommendedSuggestions + ? SuggestionsInfoCompatUtils + .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() + : 0); return new SuggestionsInfo(flags, result.mSuggestions); } catch (RuntimeException e) { // Don't kill the keyboard if there is a bug in the spell checker diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index d5b04b27c..2bc2cfdf6 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -29,65 +29,150 @@ public class SpellCheckerProximityInfo { // as the size of the passed array afterwards so they can't be different. final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; - // This is a map from the code point to the index in the PROXIMITY array. - // At the time the native code to read the binary dictionary needs the proximity info be passed - // as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input character. - // Since we need to build such an array, we want to be able to search in our big proximity data - // quickly by character, and a map is probably the best way to do this. - final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); + // Helper methods + final protected static void buildProximityIndices(final int[] proximity, + final TreeMap<Integer, Integer> indices) { + for (int i = 0; i < proximity.length; i += ROW_SIZE) { + if (NUL != proximity[i]) indices.put(proximity[i], i); + } + } + final protected static int computeIndex(final int characterCode, + final TreeMap<Integer, Integer> indices) { + final Integer result = indices.get(characterCode); + if (null == result) return -1; + return result; + } - // The proximity here is the union of - // - the proximity for a QWERTY keyboard. - // - the proximity for an AZERTY keyboard. - // - the proximity for a QWERTZ keyboard. - // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other. - // - // The reasoning behind this construction is, almost any alphabetic text we may want - // to spell check has been entered with one of the keyboards above. Also, specifically - // to English, many spelling errors consist of the last vowel of the word being wrong - // because in English vowels tend to merge with each other in pronunciation. - final public static int[] PROXIMITY = { - 'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, - 'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL, - 'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + private static class Latin { + // This is a map from the code point to the index in the PROXIMITY array. + // At the time the native code to read the binary dictionary needs the proximity info be + // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input + // character. + // Since we need to build such an array, we want to be able to search in our big proximity + // data quickly by character, and a map is probably the best way to do this. + final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); - 'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, - 's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, - 'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + // The proximity here is the union of + // - the proximity for a QWERTY keyboard. + // - the proximity for an AZERTY keyboard. + // - the proximity for a QWERTZ keyboard. + // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other. + // + // The reasoning behind this construction is, almost any alphabetic text we may want + // to spell check has been entered with one of the keyboards above. Also, specifically + // to English, many spelling errors consist of the last vowel of the word being wrong + // because in English vowels tend to merge with each other in pronunciation. + final static int[] PROXIMITY = { + 'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, + 'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL, + 'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL, - 'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - 'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, - }; - static { - for (int i = 0; i < PROXIMITY.length; i += ROW_SIZE) { - if (NUL != PROXIMITY[i]) INDICES.put(PROXIMITY[i], i); + 'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, + 's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, + 'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + + 'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL, + 'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + }; + static { + buildProximityIndices(PROXIMITY, INDICES); + } + static int getIndexOf(int characterCode) { + return computeIndex(characterCode, INDICES); } } - public static int getIndexOf(int characterCode) { - final Integer result = INDICES.get(characterCode); - if (null == result) return -1; - return result; + + private static class Cyrillic { + final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); + final static int[] PROXIMITY = { + // TODO: This table is solely based on the keyboard layout. Consult with Russian + // speakers on commonly misspelled words/letters. + 'й', 'ц', 'ф', 'ы', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ц', 'й', 'ф', 'ы', 'в', 'у', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'у', 'ц', 'ы', 'в', 'а', 'к', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'к', 'у', 'в', 'а', 'п', 'е', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'е', 'к', 'а', 'п', 'р', 'н', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'н', 'е', 'п', 'р', 'о', 'г', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'г', 'н', 'р', 'о', 'л', 'ш', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ш', 'г', 'о', 'л', 'д', 'щ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'щ', 'ш', 'л', 'д', 'ж', 'з', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'з', 'щ', 'д', 'ж', 'э', 'х', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'х', 'з', 'ж', 'э', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + + 'ф', 'й', 'ц', 'ы', 'я', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ы', 'й', 'ц', 'у', 'ф', 'в', 'я', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'в', 'ц', 'у', 'к', 'ы', 'а', 'я', 'ч', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'а', 'у', 'к', 'е', 'в', 'п', 'ч', 'с', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'п', 'к', 'е', 'н', 'а', 'р', 'с', 'м', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'р', 'е', 'н', 'г', 'п', 'о', 'м', 'и', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'о', 'н', 'г', 'ш', 'р', 'л', 'и', 'т', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'л', 'г', 'ш', 'щ', 'о', 'д', 'т', 'ь', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'д', 'ш', 'щ', 'з', 'л', 'ж', 'ь', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ж', 'щ', 'з', 'х', 'д', 'э', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'э', 'з', 'х', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + + 'я', 'ф', 'ы', 'в', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ч', 'ы', 'в', 'а', 'я', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'с', 'в', 'а', 'п', 'ч', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'м', 'а', 'п', 'р', 'с', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'и', 'п', 'р', 'о', 'м', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'т', 'р', 'о', 'л', 'и', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ь', 'о', 'л', 'д', 'т', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'б', 'л', 'д', 'ж', 'ь', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'ю', 'д', 'ж', 'э', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + }; + static { + buildProximityIndices(PROXIMITY, INDICES); + } + static int getIndexOf(int characterCode) { + return computeIndex(characterCode, INDICES); + } + } + + public static int[] getProximityForScript(final int script) { + switch (script) { + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.PROXIMITY; + default: + throw new RuntimeException("Wrong script supplied: " + script); + } + } + public static int getIndexOfCodeForScript(final int characterCode, final int script) { + switch (script) { + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.getIndexOf(characterCode); + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.getIndexOf(characterCode); + default: + throw new RuntimeException("Wrong script supplied: " + script); + } } } diff --git a/java/src/com/android/inputmethod/latin/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index 9a59ef2e0..3d26d972d 100644 --- a/java/src/com/android/inputmethod/latin/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -14,7 +14,7 @@ * the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.latin.suggestions; import android.content.res.Resources; import android.graphics.Paint; @@ -25,26 +25,27 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; -import com.android.inputmethod.keyboard.internal.KeyboardBuilder; -import com.android.inputmethod.keyboard.internal.KeyboardParams; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; public class MoreSuggestions extends Keyboard { - private static final boolean DBG = LatinImeLogger.sDBG; - public static final int SUGGESTION_CODE_BASE = 1024; private MoreSuggestions(Builder.MoreSuggestionsParam params) { super(params); } - public static class Builder extends KeyboardBuilder<Builder.MoreSuggestionsParam> { + public static class Builder extends Keyboard.Builder<Builder.MoreSuggestionsParam> { + private static final boolean DBG = LatinImeLogger.sDBG; + private final MoreSuggestionsView mPaneView; private SuggestedWords mSuggestions; private int mFromPos; private int mToPos; - public static class MoreSuggestionsParam extends KeyboardParams { + public static class MoreSuggestionsParam extends Keyboard.Params { private final int[] mWidths = new int[SuggestionsView.MAX_SUGGESTIONS]; private final int[] mRowNumbers = new int[SuggestionsView.MAX_SUGGESTIONS]; private final int[] mColumnOrders = new int[SuggestionsView.MAX_SUGGESTIONS]; @@ -176,9 +177,9 @@ public class MoreSuggestions extends Keyboard { public Builder layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth, int maxRow) { - final Keyboard keyboard = KeyboardSwitcher.getInstance().getLatinKeyboard(); + final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard(); final int xmlId = R.xml.kbd_suggestions_pane_template; - load(keyboard.mId.cloneWithNewXml(mResources.getResourceEntryName(xmlId), xmlId)); + load(xmlId, keyboard.mId); mParams.mVerticalGap = mParams.mTopPadding = keyboard.mVerticalGap / 2; final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow, diff --git a/java/src/com/android/inputmethod/latin/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index c61dd6313..b5f67ace0 100644 --- a/java/src/com/android/inputmethod/latin/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.latin.suggestions; import android.content.Context; import android.content.res.Resources; @@ -34,6 +34,7 @@ import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; import com.android.inputmethod.keyboard.PointerTracker.KeyEventHandler; import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.latin.R; /** * A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting diff --git a/java/src/com/android/inputmethod/latin/SuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java index c25ecb382..40d782640 100644 --- a/java/src/com/android/inputmethod/latin/SuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java @@ -14,7 +14,7 @@ * the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.latin.suggestions; import android.content.Context; import android.content.res.Resources; @@ -30,7 +30,6 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Message; -import android.os.SystemClock; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -58,7 +57,12 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.MoreKeysPanel; import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.StaticInnerHandlerWrapper; +import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.Utils; import java.util.ArrayList; import java.util.List; @@ -73,7 +77,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. public static final int MAX_SUGGESTIONS = 18; - private static final boolean DBG = LatinImeLogger.sDBG; + static final boolean DBG = LatinImeLogger.sDBG; private final ViewGroup mSuggestionsStrip; private KeyboardView mKeyboardView; @@ -101,8 +105,6 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> { private static final int MSG_HIDE_PREVIEW = 0; - private static final long DELAY_HIDE_PREVIEW = 1300; - public UiHandler(SuggestionsView outerInstance) { super(outerInstance); } @@ -117,11 +119,6 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, } } - public void postHidePreview() { - cancelHidePreview(); - sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW); - } - public void cancelHidePreview() { removeMessages(MSG_HIDE_PREVIEW); } @@ -149,6 +146,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, private final List<View> mDividers; private final List<TextView> mInfos; + private final int mColorValidTypedWord; private final int mColorTypedWord; private final int mColorAutoCorrect; private final int mColorSuggested; @@ -172,7 +170,6 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, public final TextView mWordToSaveView; private final TextView mHintToSaveView; - private final CharSequence mHintToSaveText; public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle, List<TextView> words, List<View> dividers, List<TextView> infos) { @@ -193,6 +190,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle); mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0); + final float alphaValidTypedWord = getPercent(a, + R.styleable.SuggestionsView_alphaValidTypedWord, 100); final float alphaTypedWord = getPercent(a, R.styleable.SuggestionsView_alphaTypedWord, 100); final float alphaAutoCorrect = getPercent(a, @@ -200,6 +199,9 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final float alphaSuggested = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100); mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100); + mColorValidTypedWord = applyAlpha( + a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0), + alphaValidTypedWord); mColorTypedWord = applyAlpha( a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord); mColorAutoCorrect = applyAlpha( @@ -228,7 +230,6 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final LayoutInflater inflater = LayoutInflater.from(context); mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null); mHintToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null); - mHintToSaveText = context.getText(R.string.hint_add_to_dictionary); } private static Drawable getMoreSuggestionsHint(Resources res, float textSize, int color) { @@ -298,6 +299,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final int color; if (index == mCenterSuggestionIndex && Utils.willAutoCorrect(suggestions)) { color = mColorAutoCorrect; + } else if (index == mCenterSuggestionIndex && suggestions.mTypedWordValid) { + color = mColorValidTypedWord; } else if (isSuggested) { color = mColorSuggested; } else { @@ -433,7 +436,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final TextView word = mWords.get(index); word.setEnabled(true); - word.setTextColor(mColorTypedWord); + word.setTextColor(mColorAutoCorrect); final CharSequence text = suggestions.getWord(index); word.setText(text); word.setTextScaleX(1.0f); @@ -445,7 +448,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, } public void layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView, - int stripWidth) { + int stripWidth, CharSequence hintText) { final int width = stripWidth - mDividerWidth - mPadding * 2; final TextView wordView = mWordToSaveView; @@ -464,13 +467,98 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, final TextView hintView = mHintToSaveView; hintView.setTextColor(mColorAutoCorrect); final int hintWidth = width - wordWidth; - final float hintScaleX = getTextScaleX(mHintToSaveText, hintWidth, hintView.getPaint()); - hintView.setText(mHintToSaveText); + final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); + hintView.setText(hintText); hintView.setTextScaleX(hintScaleX); stripView.addView(hintView); setLayoutWeight( hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); } + + private static CharSequence getDebugInfo(SuggestedWords suggestions, int pos) { + if (DBG && pos < suggestions.size()) { + final SuggestedWordInfo wordInfo = suggestions.getInfo(pos); + if (wordInfo != null) { + final CharSequence debugInfo = wordInfo.getDebugString(); + if (!TextUtils.isEmpty(debugInfo)) { + return debugInfo; + } + } + } + return null; + } + + private static void setLayoutWeight(View v, float weight, int height) { + final ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; + llp.weight = weight; + llp.width = 0; + llp.height = height; + } + } + + private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) { + paint.setTextScaleX(1.0f); + final int width = getTextWidth(text, paint); + if (width <= maxWidth) { + return 1.0f; + } + return maxWidth / (float)width; + } + + private static CharSequence getEllipsizedText(CharSequence text, int maxWidth, + TextPaint paint) { + if (text == null) return null; + paint.setTextScaleX(1.0f); + final int width = getTextWidth(text, paint); + if (width <= maxWidth) { + return text; + } + final float scaleX = maxWidth / (float)width; + if (scaleX >= MIN_TEXT_XSCALE) { + paint.setTextScaleX(scaleX); + return text; + } + + // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To + // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). + final CharSequence ellipsized = TextUtils.ellipsize( + text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE); + paint.setTextScaleX(MIN_TEXT_XSCALE); + return ellipsized; + } + + private static int getTextWidth(CharSequence text, TextPaint paint) { + if (TextUtils.isEmpty(text)) return 0; + final Typeface savedTypeface = paint.getTypeface(); + paint.setTypeface(getTextTypeface(text)); + final int len = text.length(); + final float[] widths = new float[len]; + final int count = paint.getTextWidths(text, 0, len, widths); + int width = 0; + for (int i = 0; i < count; i++) { + width += Math.round(widths[i] + 0.5f); + } + paint.setTypeface(savedTypeface); + return width; + } + + private static Typeface getTextTypeface(CharSequence text) { + if (!(text instanceof SpannableString)) + return Typeface.DEFAULT; + + final SpannableString ss = (SpannableString)text; + final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); + if (styles.length == 0) + return Typeface.DEFAULT; + + switch (styles[0].getStyle()) { + case Typeface.BOLD: return Typeface.DEFAULT_BOLD; + // TODO: BOLD_ITALIC, ITALIC case? + default: return Typeface.DEFAULT; + } + } } /** @@ -557,99 +645,15 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, mParams.layout(mSuggestions, mSuggestionsStrip, this, getWidth()); } - private static CharSequence getDebugInfo(SuggestedWords suggestions, int pos) { - if (DBG && pos < suggestions.size()) { - final SuggestedWordInfo wordInfo = suggestions.getInfo(pos); - if (wordInfo != null) { - final CharSequence debugInfo = wordInfo.getDebugString(); - if (!TextUtils.isEmpty(debugInfo)) { - return debugInfo; - } - } - } - return null; - } - - private static void setLayoutWeight(View v, float weight, int height) { - final ViewGroup.LayoutParams lp = v.getLayoutParams(); - if (lp instanceof LinearLayout.LayoutParams) { - final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; - llp.weight = weight; - llp.width = 0; - llp.height = height; - } - } - - private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) { - paint.setTextScaleX(1.0f); - final int width = getTextWidth(text, paint); - if (width <= maxWidth) { - return 1.0f; - } - return maxWidth / (float)width; - } - - private static CharSequence getEllipsizedText(CharSequence text, int maxWidth, - TextPaint paint) { - if (text == null) return null; - paint.setTextScaleX(1.0f); - final int width = getTextWidth(text, paint); - if (width <= maxWidth) { - return text; - } - final float scaleX = maxWidth / (float)width; - if (scaleX >= MIN_TEXT_XSCALE) { - paint.setTextScaleX(scaleX); - return text; - } - - // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To get - // squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). - final CharSequence ellipsized = TextUtils.ellipsize( - text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE); - paint.setTextScaleX(MIN_TEXT_XSCALE); - return ellipsized; - } - - private static int getTextWidth(CharSequence text, TextPaint paint) { - if (TextUtils.isEmpty(text)) return 0; - final Typeface savedTypeface = paint.getTypeface(); - paint.setTypeface(getTextTypeface(text)); - final int len = text.length(); - final float[] widths = new float[len]; - final int count = paint.getTextWidths(text, 0, len, widths); - int width = 0; - for (int i = 0; i < count; i++) { - width += Math.round(widths[i] + 0.5f); - } - paint.setTypeface(savedTypeface); - return width; - } - - private static Typeface getTextTypeface(CharSequence text) { - if (!(text instanceof SpannableString)) - return Typeface.DEFAULT; - - final SpannableString ss = (SpannableString)text; - final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); - if (styles.length == 0) - return Typeface.DEFAULT; - - switch (styles[0].getStyle()) { - case Typeface.BOLD: return Typeface.DEFAULT_BOLD; - // TODO: BOLD_ITALIC, ITALIC case? - default: return Typeface.DEFAULT; - } - } public boolean isShowingAddToDictionaryHint() { return mSuggestionsStrip.getChildCount() > 0 && mSuggestionsStrip.getChildAt(0) == mParams.mWordToSaveView; } - public void showAddToDictionaryHint(CharSequence word) { + public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) { clear(); - mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth()); + mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText); } public boolean dismissAddToDictionaryHint() { @@ -675,34 +679,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, mPreviewPopup.dismiss(); } - private void showPreview(View view, CharSequence word) { - if (TextUtils.isEmpty(word)) - return; - - final TextView previewText = mPreviewText; - previewText.setTextColor(mParams.mColorTypedWord); - previewText.setText(word); - previewText.measure( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - final int[] offsetInWindow = new int[2]; - view.getLocationInWindow(offsetInWindow); - final int posX = offsetInWindow[0]; - final int posY = offsetInWindow[1] - previewText.getMeasuredHeight(); - final PopupWindow previewPopup = mPreviewPopup; - if (previewPopup.isShowing()) { - previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight()); - } else { - previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY); - } - previewText.setVisibility(VISIBLE); - mHandler.postHidePreview(); - } - private void addToDictionary(CharSequence word) { - if (mListener.addWordToDictionary(word.toString())) { - final CharSequence message = getContext().getString(R.string.added_word, word); - showPreview(mParams.mWordToSaveView, message); - } + mListener.addWordToDictionary(word.toString()); } private final KeyboardActionListener mMoreSuggestionsListener = @@ -832,8 +810,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener, // Decided to be in the sliding input mode only when the touch point has been moved // upward. mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE; - tracker.onShowMoreKeysPanel( - translatedX, translatedY, SystemClock.uptimeMillis(), moreKeysPanel); + tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel); } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { // Decided to be in the modal input mode mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; |