diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
19 files changed, 1447 insertions, 778 deletions
diff --git a/java/src/com/android/inputmethod/latin/AccessibilityUtils.java b/java/src/com/android/inputmethod/latin/AccessibilityUtils.java new file mode 100644 index 000000000..cd3f9e0ad --- /dev/null +++ b/java/src/com/android/inputmethod/latin/AccessibilityUtils.java @@ -0,0 +1,211 @@ +/* + * 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.TypedArray; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardSwitcher; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility functions for accessibility support. + */ +public class AccessibilityUtils { + /** Shared singleton instance. */ + private static final AccessibilityUtils sInstance = new AccessibilityUtils(); + private /* final */ LatinIME mService; + private /* final */ AccessibilityManager mAccessibilityManager; + private /* final */ Map<Integer, CharSequence> mDescriptions; + + /** + * Returns a shared instance of AccessibilityUtils. + * + * @return A shared instance of AccessibilityUtils. + */ + public static AccessibilityUtils getInstance() { + return sInstance; + } + + /** + * Initializes (or re-initializes) the shared instance of AccessibilityUtils + * with the specified parent service and preferences. + * + * @param service The parent input method service. + * @param prefs The parent preferences. + */ + public static void init(LatinIME service, SharedPreferences prefs) { + sInstance.initialize(service, prefs); + } + + private AccessibilityUtils() { + // This class is not publicly instantiable. + } + + /** + * Initializes (or re-initializes) with the specified parent service and + * preferences. + * + * @param service The parent input method service. + * @param prefs The parent preferences. + */ + private void initialize(LatinIME service, SharedPreferences prefs) { + mService = service; + mAccessibilityManager = (AccessibilityManager) service.getSystemService( + Context.ACCESSIBILITY_SERVICE); + mDescriptions = null; + } + + /** + * Returns true if accessibility is enabled. + * + * @return {@code true} if accessibility is enabled. + */ + public boolean isAccessibilityEnabled() { + return mAccessibilityManager.isEnabled(); + } + + /** + * Speaks a key's action after it has been released. Does not speak letter + * keys since typed keys are already spoken aloud by TalkBack. + * <p> + * No-op if accessibility is not enabled. + * </p> + * + * @param primaryCode The primary code of the released key. + * @param switcher The input method's {@link KeyboardSwitcher}. + */ + public void onRelease(int primaryCode, KeyboardSwitcher switcher) { + if (!isAccessibilityEnabled()) { + return; + } + + int resId = -1; + + switch (primaryCode) { + case Keyboard.CODE_SHIFT: { + if (switcher.isShiftedOrShiftLocked()) { + resId = R.string.description_shift_on; + } else { + resId = R.string.description_shift_off; + } + break; + } + + case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: { + if (switcher.isAlphabetMode()) { + resId = R.string.description_symbols_off; + } else { + resId = R.string.description_symbols_on; + } + break; + } + } + + if (resId >= 0) { + speakDescription(mService.getResources().getText(resId)); + } + } + + /** + * Speaks a key's description for accessibility. If a key has an explicit + * description defined in keycodes.xml, that will be used. Otherwise, if the + * key is a Unicode character, then its character will be used. + * <p> + * No-op if accessibility is not enabled. + * </p> + * + * @param primaryCode The primary code of the pressed key. + * @param switcher The input method's {@link KeyboardSwitcher}. + */ + public void onPress(int primaryCode, KeyboardSwitcher switcher) { + if (!isAccessibilityEnabled()) { + return; + } + + // TODO Use the current keyboard state to read "Switch to symbols" + // instead of just "Symbols" (and similar for shift key). + CharSequence description = describeKey(primaryCode); + if (description == null && Character.isDefined((char) primaryCode)) { + description = Character.toString((char) primaryCode); + } + + if (description != null) { + speakDescription(description); + } + } + + /** + * Returns a text description for a given key code. If the key does not have + * an explicit description, returns <code>null</code>. + * + * @param keyCode An integer key code. + * @return A {@link CharSequence} describing the key or <code>null</code> if + * no description is available. + */ + private CharSequence describeKey(int keyCode) { + // If not loaded yet, load key descriptions from XML file. + if (mDescriptions == null) { + mDescriptions = loadDescriptions(); + } + + return mDescriptions.get(keyCode); + } + + /** + * Loads key descriptions from resources. + */ + private Map<Integer, CharSequence> loadDescriptions() { + final Map<Integer, CharSequence> descriptions = new HashMap<Integer, CharSequence>(); + final TypedArray array = mService.getResources().obtainTypedArray(R.array.key_descriptions); + + // Key descriptions are stored as a key code followed by a string. + for (int i = 0; i < array.length() - 1; i += 2) { + int code = array.getInteger(i, 0); + CharSequence desc = array.getText(i + 1); + + descriptions.put(code, desc); + } + + array.recycle(); + + return descriptions; + } + + /** + * Sends a character sequence to be read aloud. + * + * @param description The {@link CharSequence} to be read aloud. + */ + private void speakDescription(CharSequence description) { + // TODO We need to add an AccessibilityEvent type for IMEs. + final AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + event.setPackageName(mService.getPackageName()); + event.setClassName(getClass().getName()); + event.setAddedCount(description.length()); + event.getText().add(description); + + mAccessibilityManager.sendAccessibilityEvent(event); + } +} diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java new file mode 100644 index 000000000..092f7ad63 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -0,0 +1,143 @@ +/* + * 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.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Map; + +public class AutoCorrection { + private static final boolean DBG = LatinImeLogger.sDBG; + private static final String TAG = AutoCorrection.class.getSimpleName(); + private boolean mHasAutoCorrection; + private CharSequence mAutoCorrectionWord; + private double mNormalizedScore; + + public void init() { + mHasAutoCorrection = false; + mAutoCorrectionWord = null; + mNormalizedScore = Integer.MIN_VALUE; + } + + public boolean hasAutoCorrection() { + return mHasAutoCorrection; + } + + public CharSequence getAutoCorrectionWord() { + return mAutoCorrectionWord; + } + + public double getNormalizedScore() { + return mNormalizedScore; + } + + public void updateAutoCorrectionStatus(Map<String, Dictionary> dictionaries, + WordComposer wordComposer, ArrayList<CharSequence> suggestions, int[] priorities, + CharSequence typedWord, double autoCorrectionThreshold, int correctionMode, + CharSequence quickFixedWord, CharSequence whitelistedWord) { + if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) { + mHasAutoCorrection = true; + mAutoCorrectionWord = whitelistedWord; + } else if (hasAutoCorrectionForTypedWord( + dictionaries, wordComposer, suggestions, typedWord, correctionMode)) { + mHasAutoCorrection = true; + mAutoCorrectionWord = typedWord; + } else if (hasAutoCorrectionForQuickFix(quickFixedWord)) { + mHasAutoCorrection = true; + mAutoCorrectionWord = quickFixedWord; + } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions, correctionMode, + priorities, typedWord, autoCorrectionThreshold)) { + mHasAutoCorrection = true; + mAutoCorrectionWord = suggestions.get(0); + } + } + + public static boolean isValidWord( + Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) { + if (TextUtils.isEmpty(word)) { + return false; + } + final CharSequence lowerCasedWord = word.toString().toLowerCase(); + for (final String key : dictionaries.keySet()) { + if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue; + final Dictionary dictionary = dictionaries.get(key); + if (dictionary.isValidWord(word) + || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { + return true; + } + } + return false; + } + + public static boolean isValidWordForAutoCorrection( + Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) { + final Dictionary whiteList = dictionaries.get(Suggest.DICT_KEY_WHITELIST); + // If "word" is in the whitelist dictionary, it should not be auto corrected. + if (whiteList != null && whiteList.isValidWord(word)) { + return false; + } + return isValidWord(dictionaries, word, ignoreCase); + } + + private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) { + return whiteListedWord != null; + } + + private boolean hasAutoCorrectionForTypedWord(Map<String, Dictionary> dictionaries, + WordComposer wordComposer, ArrayList<CharSequence> suggestions, CharSequence typedWord, + int correctionMode) { + if (TextUtils.isEmpty(typedWord)) return false; + boolean isValidWord = isValidWordForAutoCorrection(dictionaries, typedWord, false); + return wordComposer.size() > 1 && suggestions.size() > 0 && isValidWord + && (correctionMode == Suggest.CORRECTION_FULL + || correctionMode == Suggest.CORRECTION_FULL_BIGRAM); + } + + private static boolean hasAutoCorrectionForQuickFix(CharSequence quickFixedWord) { + return quickFixedWord != null; + } + + private boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer, + ArrayList<CharSequence> suggestions, int correctionMode, int[] priorities, + CharSequence typedWord, double autoCorrectionThreshold) { + if (wordComposer.size() > 1 && (correctionMode == Suggest.CORRECTION_FULL + || correctionMode == Suggest.CORRECTION_FULL_BIGRAM) + && typedWord != null && suggestions.size() > 0 && priorities.length > 0) { + final CharSequence autoCorrectionCandidate = suggestions.get(0); + final int autoCorrectionCandidateScore = priorities[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,autoCorrectionCandidate, autoCorrectionCandidateScore); + if (DBG) { + Log.d(TAG, "Normalized " + typedWord + "," + autoCorrectionCandidate + "," + + autoCorrectionCandidateScore + ", " + mNormalizedScore + + "(" + autoCorrectionThreshold + ")"); + } + if (mNormalizedScore >= autoCorrectionThreshold) { + if (DBG) { + Log.d(TAG, "Auto corrected by S-threshold."); + } + return true; + } + } + return false; + } + +} diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 307b81d43..a00b0915c 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -27,6 +27,7 @@ import android.provider.BaseColumns; import android.util.Log; import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 813f7d32f..08ddd25fa 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -16,10 +16,15 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.ProximityInfo; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.util.Log; +import java.io.File; import java.util.Arrays; /** @@ -33,11 +38,11 @@ public class BinaryDictionary extends Dictionary { * It is necessary to keep it at this value because some languages e.g. German have * really long words. */ - protected static final int MAX_WORD_LENGTH = 48; + public static final int MAX_WORD_LENGTH = 48; + public static final int MAX_WORDS = 18; private static final String TAG = "BinaryDictionary"; - private static final int MAX_ALTERNATIVES = 16; - private static final int MAX_WORDS = 18; + private static final int MAX_PROXIMITY_CHARS_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; private static final int MAX_BIGRAMS = 60; private static final int TYPED_LETTER_MULTIPLIER = 2; @@ -46,19 +51,32 @@ public class BinaryDictionary extends Dictionary { private int mDicTypeId; private int mNativeDict; private long mDictLength; - private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; + 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]; private final int[] mFrequencies = new int[MAX_WORDS]; private final int[] mFrequencies_bigrams = new int[MAX_BIGRAMS]; - static { - try { - System.loadLibrary("jni_latinime"); - } catch (UnsatisfiedLinkError ule) { - Log.e(TAG, "Could not load native library jni_latinime"); + private final KeyboardSwitcher mKeyboardSwitcher = KeyboardSwitcher.getInstance(); + private final SubtypeSwitcher mSubtypeSwitcher = SubtypeSwitcher.getInstance(); + + private static class Flags { + private static class FlagEntry { + public final String mName; + public final int mValue; + public FlagEntry(String name, int value) { + mName = name; + mValue = value; + } } + public static final FlagEntry[] ALL_FLAGS = { + // Here should reside all flags that trigger some special processing + // These *must* match the definition in UnigramDictionary enum in + // unigram_dictionary.h so please update both at the same time. + new FlagEntry("requiresGermanUmlautProcessing", 0x1) + }; } + private int mFlags = 0; private BinaryDictionary() { } @@ -72,47 +90,80 @@ public class BinaryDictionary extends Dictionary { public static BinaryDictionary initDictionary(Context context, int resId, int dicTypeId) { synchronized (sInstance) { sInstance.closeInternal(); - if (resId != 0) { - sInstance.loadDictionary(context, resId); + try { + final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); + if (afd == null) { + Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); + return null; + } + final String sourceDir = context.getApplicationInfo().sourceDir; + final File packagePath = new File(sourceDir); + // TODO: Come up with a way to handle a directory. + if (!packagePath.isFile()) { + Log.e(TAG, "sourceDir is not a file: " + sourceDir); + return null; + } + sInstance.loadDictionary(sourceDir, afd.getStartOffset(), afd.getLength()); + sInstance.mDicTypeId = dicTypeId; + } catch (android.content.res.Resources.NotFoundException e) { + Log.e(TAG, "Could not find the resource. resId=" + resId); + return null; + } + } + sInstance.initFlags(); + return sInstance; + } + + /* package for test */ static BinaryDictionary initDictionary(File dictionary, long startOffset, + long length, int dicTypeId) { + synchronized (sInstance) { + sInstance.closeInternal(); + if (dictionary.isFile()) { + sInstance.loadDictionary(dictionary.getAbsolutePath(), startOffset, length); sInstance.mDicTypeId = dicTypeId; + } else { + Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath()); + return null; } } return sInstance; } + private void initFlags() { + int flags = 0; + for (Flags.FlagEntry entry : Flags.ALL_FLAGS) { + if (mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(entry.mName)) + flags |= entry.mValue; + } + mFlags = flags; + } + + static { + Utils.loadNativeLibrary(); + } + private native int 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[] inputCodes, int codesSize, - char[] outputChars, int[] frequencies, - int[] nextLettersFrequencies, int nextLettersSize); + private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates, + int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars, + int[] frequencies); private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies, int maxWordLength, int maxBigrams, int maxAlternatives); - private final void loadDictionary(Context context, int resId) { - try { - final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); - if (afd == null) { - Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); - return; - } - mNativeDict = openNative(context.getApplicationInfo().sourceDir, - afd.getStartOffset(), afd.getLength(), + private final void loadDictionary(String path, long startOffset, long length) { + mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES); - mDictLength = afd.getLength(); - } catch (android.content.res.Resources.NotFoundException e) { - Log.e(TAG, "Could not find the resource. resId=" + resId); - return; - } + MAX_WORD_LENGTH, MAX_WORDS, MAX_PROXIMITY_CHARS_SIZE); + mDictLength = length; } @Override public void getBigrams(final WordComposer codes, final CharSequence previousWord, - final WordCallback callback, int[] nextLettersFrequencies) { + final WordCallback callback) { if (mNativeDict == 0) return; char[] chars = previousWord.toString().toCharArray(); @@ -123,11 +174,11 @@ public class BinaryDictionary extends Dictionary { Arrays.fill(mInputCodes, -1); int[] alternatives = codes.getCodesAt(0); System.arraycopy(alternatives, 0, mInputCodes, 0, - Math.min(alternatives.length, MAX_ALTERNATIVES)); + Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE)); int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize, mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS, - MAX_ALTERNATIVES); + MAX_PROXIMITY_CHARS_SIZE); for (int j = 0; j < count; ++j) { if (mFrequencies_bigrams[j] < 1) break; @@ -144,26 +195,9 @@ public class BinaryDictionary extends Dictionary { } @Override - public void getWords(final WordComposer codes, final WordCallback callback, - int[] nextLettersFrequencies) { - if (mNativeDict == 0) return; - - final int codesSize = codes.size(); - // Won't deal with really long words. - if (codesSize > MAX_WORD_LENGTH - 1) return; - - Arrays.fill(mInputCodes, -1); - for (int i = 0; i < codesSize; i++) { - int[] alternatives = codes.getCodesAt(i); - System.arraycopy(alternatives, 0, mInputCodes, i * MAX_ALTERNATIVES, - Math.min(alternatives.length, MAX_ALTERNATIVES)); - } - Arrays.fill(mOutputChars, (char) 0); - Arrays.fill(mFrequencies, 0); - - int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, - mFrequencies, nextLettersFrequencies, - nextLettersFrequencies != null ? nextLettersFrequencies.length : 0); + public void getWords(final WordComposer codes, final WordCallback callback) { + final int count = getSuggestions(codes, mKeyboardSwitcher.getLatinKeyboard(), + mOutputChars, mFrequencies); for (int j = 0; j < count; ++j) { if (mFrequencies[j] < 1) break; @@ -179,6 +213,33 @@ public class BinaryDictionary extends Dictionary { } } + /* package for test */ boolean isValidDictionary() { + return mNativeDict != 0; + } + + /* package for test */ int getSuggestions(final WordComposer codes, final Keyboard keyboard, + char[] outputChars, int[] frequencies) { + if (!isValidDictionary()) return -1; + + final int codesSize = codes.size(); + // Won't deal with really long words. + if (codesSize > MAX_WORD_LENGTH - 1) return -1; + + Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE); + for (int i = 0; i < codesSize; i++) { + int[] alternatives = codes.getCodesAt(i); + System.arraycopy(alternatives, 0, mInputCodes, i * MAX_PROXIMITY_CHARS_SIZE, + Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE)); + } + Arrays.fill(outputChars, (char) 0); + Arrays.fill(frequencies, 0); + + return getSuggestionsNative( + mNativeDict, keyboard.getProximityInfo(), + codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize, + mFlags, outputChars, frequencies); + } + @Override public boolean isValidWord(CharSequence word) { if (word == null) return false; diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index fc45c7c75..5719b9012 100644 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -54,7 +54,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); private static final int MAX_SUGGESTIONS = 16; - private static boolean DBG = LatinImeLogger.sDBG; + private static final boolean DBG = LatinImeLogger.sDBG; private final ArrayList<View> mWords = new ArrayList<View>(); private final boolean mConfigCandidateHighlightFontColorEnabled; @@ -226,10 +226,14 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } final String debugString = info.getDebugString(); if (DBG) { - if (!TextUtils.isEmpty(debugString)) { + if (TextUtils.isEmpty(debugString)) { + dv.setVisibility(GONE); + } else { dv.setText(debugString); dv.setVisibility(VISIBLE); } + } else { + dv.setVisibility(GONE); } } else { dv.setVisibility(GONE); @@ -249,8 +253,10 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo final TextView tv = (TextView)mWords.get(1).findViewById(R.id.candidate_word); final Spannable word = new SpannableString(autoCorrectedWord); final int wordLength = word.length(); - word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - word.setSpan(mInvertedForegroundColorSpan, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + word.setSpan(mInvertedForegroundColorSpan, 0, wordLength, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); tv.setText(word); mShowingAutoCorrectionInverted = true; } @@ -341,9 +347,6 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo if (mShowingAddToDictionary && index == 0) { addToDictionary(word); } else { - if (!mSuggestions.mIsApplicationSpecifiedCompletions) { - TextEntryState.acceptedSuggestion(mSuggestions.getWord(0), word); - } mService.pickSuggestionManually(index, word); } } diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java index 03211f36b..2f1e7c2b8 100644 --- a/java/src/com/android/inputmethod/latin/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/DebugSettings.java @@ -20,6 +20,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; +import android.os.Process; import android.preference.CheckBoxPreference; import android.preference.PreferenceActivity; import android.util.Log; @@ -30,6 +31,7 @@ public class DebugSettings extends PreferenceActivity private static final String TAG = "DebugSettings"; private static final String DEBUG_MODE_KEY = "debug_mode"; + private boolean mServiceNeedsRestart = false; private CheckBoxPreference mDebugMode; @Override @@ -39,16 +41,24 @@ public class DebugSettings extends PreferenceActivity SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); prefs.registerOnSharedPreferenceChangeListener(this); + mServiceNeedsRestart = false; mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY); updateDebugMode(); } @Override + protected void onStop() { + super.onStop(); + if (mServiceNeedsRestart) Process.killProcess(Process.myPid()); + } + + @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key.equals(DEBUG_MODE_KEY)) { if (mDebugMode != null) { mDebugMode.setChecked(prefs.getBoolean(DEBUG_MODE_KEY, false)); updateDebugMode(); + mServiceNeedsRestart = true; } } } diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 74933595c..56f0cc503 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -61,14 +61,9 @@ public abstract class Dictionary { * words are added through the callback object. * @param composer the key sequence to match * @param callback the callback object to send matched words to as possible candidates - * @param nextLettersFrequencies array of frequencies of next letters that could follow the - * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have - * a non-zero value on returning from this method. - * Pass in null if you don't want the dictionary to look up next letters. * @see WordCallback#addWord(char[], int, int) */ - abstract public void getWords(final WordComposer composer, final WordCallback callback, - int[] nextLettersFrequencies); + abstract public void getWords(final WordComposer composer, final WordCallback callback); /** * Searches for pairs in the bigram dictionary that matches the previous word and all the @@ -76,13 +71,9 @@ public abstract class Dictionary { * @param composer the key sequence to match * @param previousWord the word before * @param callback the callback object to send possible word following previous word - * @param nextLettersFrequencies array of frequencies of next letters that could follow the - * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have - * a non-zero value on returning from this method. - * Pass in null if you don't want the dictionary to look up next letters. */ public void getBigrams(final WordComposer composer, final CharSequence previousWord, - final WordCallback callback, int[] nextLettersFrequencies) { + final WordCallback callback) { // empty base implementation } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 0fc86c335..0318175f6 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -37,7 +37,6 @@ public class ExpandableDictionary extends Dictionary { private int mDicTypeId; private int mMaxDepth; private int mInputLength; - private int[] mNextLettersFrequencies; private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH); private static final char QUOTE = '\''; @@ -191,8 +190,7 @@ public class ExpandableDictionary extends Dictionary { } @Override - public void getWords(final WordComposer codes, final WordCallback callback, - int[] nextLettersFrequencies) { + public void getWords(final WordComposer codes, final WordCallback callback) { synchronized (mUpdatingLock) { // If we need to update, start off a background task if (mRequiresReload) startDictionaryLoadingTaskLocked(); @@ -201,7 +199,6 @@ public class ExpandableDictionary extends Dictionary { } mInputLength = codes.size(); - mNextLettersFrequencies = nextLettersFrequencies; if (mCodes.length < mInputLength) mCodes = new int[mInputLength][]; // Cache the codes so that we don't have to lookup an array list for (int i = 0; i < mInputLength; i++) { @@ -228,11 +225,21 @@ public class ExpandableDictionary extends Dictionary { /** * Returns the word's frequency or -1 if not found */ - public int getWordFrequency(CharSequence word) { + protected int getWordFrequency(CharSequence word) { Node node = searchNode(mRoots, word, 0, word.length()); return (node == null) ? -1 : node.mFrequency; } + private static int computeSkippedWordFinalFreq(int freq, int snr, int inputLength) { + // The computation itself makes sense for >= 2, but the == 2 case returns 0 + // anyway so we may as well test against 3 instead and return the constant + if (inputLength >= 3) { + return (freq * snr * (inputLength - 2)) / (inputLength - 1); + } else { + return 0; + } + } + /** * Recursively traverse the tree for words that match the input. Input consists of * a list of arrays. Each item in the list is one input character position. An input @@ -246,13 +253,14 @@ public class ExpandableDictionary extends Dictionary { * @param completion whether the traversal is now in completion mode - meaning that we've * exhausted the input and we're looking for all possible suffixes. * @param snr current weight of the word being formed - * @param inputIndex position in the input characters. This can be off from the depth in + * @param inputIndex position in the input characters. This can be off from the depth in * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type * "wouldve", it could be matching "would've", so the depth will be one more than the * inputIndex * @param callback the callback class for adding a word */ - protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word, + // TODO: Share this routine with the native code for BinaryDictionary + protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word, final int depth, boolean completion, int snr, int inputIndex, int skipPos, WordCallback callback) { final int count = roots.mLength; @@ -278,14 +286,15 @@ public class ExpandableDictionary extends Dictionary { if (completion) { word[depth] = c; if (terminal) { - if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId, - DataType.UNIGRAM)) { - return; + final int finalFreq; + if (skipPos < 0) { + finalFreq = freq * snr; + } else { + finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength); } - // Add to frequency of next letters for predictive correction - if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0 - && mNextLettersFrequencies.length > word[inputIndex]) { - mNextLettersFrequencies[word[inputIndex]]++; + if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, + DataType.UNIGRAM)) { + return; } } if (children != null) { @@ -296,7 +305,7 @@ public class ExpandableDictionary extends Dictionary { // Skip the ' and continue deeper word[depth] = c; if (children != null) { - getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, + getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, skipPos, callback); } } else { @@ -313,10 +322,16 @@ public class ExpandableDictionary extends Dictionary { if (codeSize == inputIndex + 1) { if (terminal) { - if (INCLUDE_TYPED_WORD_IF_VALID + if (INCLUDE_TYPED_WORD_IF_VALID || !same(word, depth + 1, codes.getTypedWord())) { - int finalFreq = freq * snr * addedAttenuation; - if (skipPos < 0) finalFreq *= FULL_WORD_FREQ_MULTIPLIER; + final int finalFreq; + if (skipPos < 0) { + finalFreq = freq * snr * addedAttenuation + * FULL_WORD_FREQ_MULTIPLIER; + } else { + finalFreq = computeSkippedWordFinalFreq(freq, + snr * addedAttenuation, mInputLength); + } callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, DataType.UNIGRAM); } @@ -327,7 +342,7 @@ public class ExpandableDictionary extends Dictionary { skipPos, callback); } } else if (children != null) { - getWordsRec(children, codes, word, depth + 1, + getWordsRec(children, codes, word, depth + 1, false, snr * addedAttenuation, inputIndex + 1, skipPos, callback); } @@ -427,7 +442,7 @@ public class ExpandableDictionary extends Dictionary { @Override public void getBigrams(final WordComposer codes, final CharSequence previousWord, - final WordCallback callback, int[] nextLettersFrequencies) { + final WordCallback callback) { if (!reloadDictionaryIfRequired()) { runReverseLookUp(previousWord, callback); } @@ -516,7 +531,7 @@ public class ExpandableDictionary extends Dictionary { } } - static char toLowerCase(char c) { + private static char toLowerCase(char c) { char baseChar = c; if (c < BASE_CHARS.length) { baseChar = BASE_CHARS[c]; @@ -535,7 +550,7 @@ public class ExpandableDictionary extends Dictionary { * if c is not a combined character, or the base character if it * is combined. */ - static final char BASE_CHARS[] = { + private static final char BASE_CHARS[] = { 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index a9f2c2c22..5587c685f 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -106,7 +106,7 @@ public class InputLanguageSelection extends PreferenceActivity { conf.locale = locale; res.updateConfiguration(conf, res.getDisplayMetrics()); - int mainDicResId = LatinIME.getMainDictionaryResourceId(res); + int mainDicResId = Utils.getMainDictionaryResourceId(res); BinaryDictionary bd = BinaryDictionary.initDictionary(this, mainDicResId, Suggest.DIC_MAIN); // Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 5ce1b7e95..6e76cadf2 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin; 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; @@ -40,6 +39,7 @@ import android.media.AudioManager; import android.net.ConnectivityManager; import android.os.Debug; import android.os.Handler; +import android.os.IBinder; import android.os.Message; import android.os.SystemClock; import android.os.Vibrator; @@ -81,13 +81,34 @@ import java.util.Locale; /** * Input method implementation for Qwerty'ish keyboard. */ -public class LatinIME extends InputMethodService implements KeyboardActionListener, - SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "LatinIME"; +public class LatinIME extends InputMethodService implements KeyboardActionListener { + 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 = LatinImeLogger.sDBG; + /** + * The private IME option used to indicate that no microphone should be + * shown for a given text field. For instance, this is specified by the + * search dialog when the dialog is already showing a voice search button. + * + * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed. + */ + public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm"; + + /** + * The private IME option used to indicate that no microphone should be + * shown for a given text field. For instance, this is specified by the + * search dialog when the dialog is already showing a voice search button. + */ + public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey"; + + /** + * The private IME option used to indicate that no settings key should be + * shown for a given text field. + */ + public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey"; + private static final int DELAY_UPDATE_SUGGESTIONS = 180; private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300; private static final int DELAY_UPDATE_SHIFT_STATE = 300; @@ -138,6 +159,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mIsSettingsSuggestionStripOn; private boolean mApplicationSpecifiedCompletionOn; + private AccessibilityUtils mAccessibilityUtils; + private final StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); private CharSequence mBestWord; @@ -145,7 +168,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mHasDictionary; private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; - private boolean mReCorrectionEnabled; + private boolean mRecorrectionEnabled; private boolean mBigramSuggestionEnabled; private boolean mAutoCorrectOn; private boolean mVibrateOn; @@ -158,6 +181,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private int mConfigDelayBeforeFadeoutLanguageOnSpacebar; private int mConfigDurationOfFadeoutLanguageOnSpacebar; private float mConfigFinalFadeoutFactorOfLanguageOnSpacebar; + private long mConfigDoubleSpacesTurnIntoPeriodTimeout; private int mCorrectionMode; private int mCommittedLength; @@ -169,7 +193,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; - private boolean mJustReverted; private int mDeleteCount; private long mLastKeyTime; @@ -186,7 +209,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; - private boolean mRefreshKeyboardRequired; private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); @@ -247,6 +269,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_VOICE_RESULTS = 3; private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4; private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5; + private static final int MSG_SPACE_TYPED = 6; @Override public void handleMessage(Message msg) { @@ -323,7 +346,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); if (inputView != null) { - final LatinKeyboard keyboard = inputView.getLatinKeyboard(); + final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard(); // The language is never displayed when the delay is zero. if (mConfigDelayBeforeFadeoutLanguageOnSpacebar != 0) inputView.setSpacebarTextFadeFactor(localeChanged ? 1.0f @@ -335,6 +358,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } + + public void startDoubleSpacesTimer() { + removeMessages(MSG_SPACE_TYPED); + sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), + mConfigDoubleSpacesTurnIntoPeriodTimeout); + } + + public void cancelDoubleSpacesTimer() { + removeMessages(MSG_SPACE_TYPED); + } + + public boolean isAcceptingDoubleSpaces() { + return hasMessages(MSG_SPACE_TYPED); + } } @Override @@ -344,13 +381,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen LatinImeLogger.init(this, prefs); SubtypeSwitcher.init(this, prefs); KeyboardSwitcher.init(this, prefs); + AccessibilityUtils.init(this, prefs); super.onCreate(); mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); - mInputMethodId = Utils.getInputMethodId(mImm, getApplicationInfo().packageName); + mInputMethodId = Utils.getInputMethodId(mImm, getPackageName()); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); + mAccessibilityUtils = AccessibilityUtils.getInstance(); final Resources res = getResources(); mResources = res; @@ -358,10 +397,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // If the option should not be shown, do not read the recorrection preference // but always use the default setting defined in the resources. if (res.getBoolean(R.bool.config_enable_show_recorrection_option)) { - mReCorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED, - res.getBoolean(R.bool.default_recorrection_enabled)); + mRecorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED, + res.getBoolean(R.bool.config_default_recorrection_enabled)); } else { - mReCorrectionEnabled = res.getBoolean(R.bool.default_recorrection_enabled); + mRecorrectionEnabled = res.getBoolean(R.bool.config_default_recorrection_enabled); } mConfigEnableShowSubtypeSettings = res.getBoolean( @@ -374,6 +413,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen R.integer.config_duration_of_fadeout_language_on_spacebar); mConfigFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger( R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; + mConfigDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( + R.integer.config_double_spaces_turn_into_period_timeout); Utils.GCUtils.getInstance().reset(); boolean tryGC = true; @@ -395,21 +436,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); registerReceiver(mReceiver, filter); mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); - prefs.registerOnSharedPreferenceChangeListener(this); - } - - /** - * Returns a main dictionary resource id - * @return main dictionary resource id - */ - public static int getMainDictionaryResourceId(Resources res) { - final String MAIN_DIC_NAME = "main"; - String packageName = LatinIME.class.getPackage().getName(); - return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName); } private void initSuggest() { - updateAutoTextEnabled(); String locale = mSubtypeSwitcher.getInputLocaleStr(); Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); @@ -420,9 +449,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mQuickFixes = isQuickFixesEnabled(prefs); final Resources res = mResources; - int mainDicResId = getMainDictionaryResourceId(res); + int mainDicResId = Utils.getMainDictionaryResourceId(res); mSuggest = new Suggest(this, mainDicResId); loadAndSetAutoCorrectionThreshold(prefs); + updateAutoTextEnabled(); mUserDictionary = new UserDictionary(this, locale); mSuggest.setUserDictionary(mUserDictionary); @@ -497,17 +527,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return container; } - private static boolean isPasswordVariation(int variation) { - return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD - || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; - } - - private static boolean isEmailVariation(int variation) { - return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; - } - @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { final KeyboardSwitcher switcher = mKeyboardSwitcher; @@ -523,20 +542,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeSwitcher.updateParametersOnStartInputView(); - if (mRefreshKeyboardRequired) { - mRefreshKeyboardRequired = false; - onRefreshKeyboard(); - } - - TextEntryState.newSession(this); + 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. - mVoiceConnector.resetVoiceStates(isPasswordVariation( - attribute.inputType & InputType.TYPE_MASK_VARIATION)); + final VoiceIMEConnector voiceIme = mVoiceConnector; + voiceIme.resetVoiceStates(Utils.isPasswordInputType(attribute.inputType) + || Utils.isVisiblePasswordInputType(attribute.inputType)); - final int mode = initializeInputAttributesAndGetMode(attribute.inputType); + initializeInputAttributes(attribute); inputView.closing(); mEnteredText = null; @@ -547,9 +562,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen loadSettings(attribute); if (mSubtypeSwitcher.isKeyboardMode()) { - switcher.loadKeyboard(mode, attribute.imeOptions, - mVoiceConnector.isVoiceButtonEnabled(), - mVoiceConnector.isVoiceButtonOnPrimary()); + switcher.loadKeyboard(attribute, + mSubtypeSwitcher.isShortcutImeEnabled() && voiceIme.isVoiceButtonEnabled(), + voiceIme.isVoiceButtonOnPrimary()); switcher.updateShiftState(); } @@ -560,18 +575,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen updateCorrectionMode(); + final boolean accessibilityEnabled = mAccessibilityUtils.isAccessibilityEnabled(); + inputView.setPreviewEnabled(mPopupOn); inputView.setProximityCorrectionEnabled(true); + inputView.setAccessibilityEnabled(accessibilityEnabled); // If we just entered a text field, maybe it has some old text that requires correction - checkReCorrectionOnStart(); + checkRecorrectionOnStart(); inputView.setForeground(true); - mVoiceConnector.onStartInputView(inputView.getWindowToken()); + voiceIme.onStartInputView(inputView.getWindowToken()); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } - private int initializeInputAttributesAndGetMode(int inputType) { + private void initializeInputAttributes(EditorInfo attribute) { + if (attribute == null) + return; + final int inputType = attribute.inputType; final int variation = inputType & InputType.TYPE_MASK_VARIATION; mAutoSpace = false; mInputTypeNoAutoCorrect = false; @@ -579,73 +600,52 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mApplicationSpecifiedCompletionOn = false; mApplicationSpecifiedCompletions = null; - final int mode; - switch (inputType & InputType.TYPE_MASK_CLASS) { - case InputType.TYPE_CLASS_NUMBER: - case InputType.TYPE_CLASS_DATETIME: - mode = KeyboardId.MODE_NUMBER; - break; - case InputType.TYPE_CLASS_PHONE: - mode = KeyboardId.MODE_PHONE; - break; - case InputType.TYPE_CLASS_TEXT: - mIsSettingsSuggestionStripOn = true; - // Make sure that passwords are not displayed in candidate view - if (isPasswordVariation(variation)) { - mIsSettingsSuggestionStripOn = false; - } - if (isEmailVariation(variation) - || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { - mAutoSpace = false; - } else { - mAutoSpace = true; - } - if (isEmailVariation(variation)) { - mIsSettingsSuggestionStripOn = false; - mode = KeyboardId.MODE_EMAIL; - } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { - mIsSettingsSuggestionStripOn = false; - mode = KeyboardId.MODE_URL; - } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { - mode = KeyboardId.MODE_IM; - } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - mIsSettingsSuggestionStripOn = false; - mode = KeyboardId.MODE_TEXT; - } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { - mode = KeyboardId.MODE_WEB; - // 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; - } - } else { - mode = KeyboardId.MODE_TEXT; - } - - // 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) { + if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { + mIsSettingsSuggestionStripOn = true; + // Make sure that passwords are not displayed in candidate view + if (Utils.isPasswordInputType(inputType) + || Utils.isVisiblePasswordInputType(inputType)) { + mIsSettingsSuggestionStripOn = false; + } + if (Utils.isEmailVariation(variation) + || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { + mAutoSpace = false; + } else { + mAutoSpace = true; + } + if (Utils.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 ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { - mIsSettingsSuggestionStripOn = false; - mApplicationSpecifiedCompletionOn = isFullscreenMode(); - } - break; - default: - mode = KeyboardId.MODE_TEXT; - break; + } + + // 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(); + } } - return mode; } - private void checkReCorrectionOnStart() { - if (!mReCorrectionEnabled) return; + private void checkRecorrectionOnStart() { + if (!mRecorrectionEnabled) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -713,6 +713,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (DEBUG) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + + ", lss=" + mLastSelectionStart + + ", lse=" + mLastSelectionEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + candidatesStart @@ -723,9 +725,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // If the current selection in the text view changes, we should // clear whatever candidate text we have. - if ((((mComposing.length() > 0 && mHasValidSuggestions) - || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd - || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) { + final boolean selectionChanged = (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart; + final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1; + if (((mComposing.length() > 0 && mHasValidSuggestions) + || mVoiceConnector.isVoiceInputHighlighted()) + && (selectionChanged || candidatesCleared)) { + if (candidatesCleared) { + // If the composing span has been cleared, save the typed word in the history for + // recorrection before we reset the candidate strip. Then, we'll be able to show + // suggestions for recorrection right away. + saveWordInHistory(mComposing); + } mComposing.setLength(0); mHasValidSuggestions = false; mHandler.postUpdateSuggestions(); @@ -736,15 +747,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mVoiceConnector.setVoiceInputHighlighted(false); } else if (!mHasValidSuggestions && !mJustAccepted) { - switch (TextEntryState.getState()) { - case ACCEPTED_DEFAULT: - TextEntryState.reset(); - // $FALL-THROUGH$ - case SPACE_AFTER_PICKED: + if (TextEntryState.isAcceptedDefault() || TextEntryState.isSpaceAfterPicked()) { + if (TextEntryState.isAcceptedDefault()) + TextEntryState.reset(); mJustAddedAutoSpace = false; // The user moved the cursor. - break; - default: - break; } } mJustAccepted = false; @@ -754,18 +760,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; - if (mReCorrectionEnabled && isShowingSuggestionsStrip()) { + if (mRecorrectionEnabled && isShowingSuggestionsStrip()) { // Don't look for corrections if the keyboard is not visible if (mKeyboardSwitcher.isInputViewShown()) { // Check if we should go in or out of correction mode. - if (isSuggestionsRequested() && !mJustReverted + if (isSuggestionsRequested() && (candidatesStart == candidatesEnd || newSelStart != oldSelStart - || TextEntryState.isCorrecting()) + || TextEntryState.isRecorrecting()) && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) { if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { mHandler.postUpdateOldSuggestions(); } else { - abortCorrection(false); + abortRecorrection(false); // Show the punctuation suggestions list if the current one is not // and if not showing "Touch again to save". if (mCandidateView != null && !isShowingPunctuationList() @@ -788,7 +794,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedTextClicked() { - if (mReCorrectionEnabled && isSuggestionsRequested()) return; + if (mRecorrectionEnabled && isSuggestionsRequested()) return; super.onExtractedTextClicked(); } @@ -804,7 +810,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedCursorMovement(int dx, int dy) { - if (mReCorrectionEnabled && isSuggestionsRequested()) return; + if (mRecorrectionEnabled && isSuggestionsRequested()) return; super.onExtractedCursorMovement(dx, dy); } @@ -822,17 +828,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mVoiceConnector.hideVoiceWindow(mConfigurationChanging); mWordHistory.clear(); super.hideWindow(); - TextEntryState.endSession(); } @Override public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { if (DEBUG) { Log.i(TAG, "Received completions:"); - final int count = (applicationSpecifiedCompletions != null) - ? applicationSpecifiedCompletions.length : 0; - for (int i = 0; i < count; i++) { - Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); + if (applicationSpecifiedCompletions != null) { + for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { + Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); + } } } if (mApplicationSpecifiedCompletionOn) { @@ -963,7 +968,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mCommittedLength = mComposing.length(); TextEntryState.acceptedTyped(mComposing); - addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); + addToAutoAndUserBigramDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); } updateSuggestions(); } @@ -1011,7 +1016,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void doubleSpace() { - //if (!mAutoPunctuate) return; if (mCorrectionMode == Suggest.CORRECTION_NONE) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -1019,13 +1023,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (lastThree != null && lastThree.length() == 3 && Character.isLetterOrDigit(lastThree.charAt(0)) && lastThree.charAt(1) == Keyboard.CODE_SPACE - && lastThree.charAt(2) == Keyboard.CODE_SPACE) { + && lastThree.charAt(2) == Keyboard.CODE_SPACE + && mHandler.isAcceptingDoubleSpaces()) { + mHandler.cancelDoubleSpacesTimer(); ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mJustAddedAutoSpace = true; + } else { + mHandler.startDoubleSpacesTimer(); } } @@ -1105,6 +1113,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; KeyboardSwitcher switcher = mKeyboardSwitcher; + final boolean accessibilityEnabled = switcher.isAccessibilityEnabled(); final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); switch (primaryCode) { case Keyboard.CODE_DELETE: @@ -1114,12 +1123,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen break; case Keyboard.CODE_SHIFT: // Shift key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) + if (!distinctMultiTouch || accessibilityEnabled) switcher.toggleShift(); break; case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: // Symbol key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) + if (!distinctMultiTouch || accessibilityEnabled) switcher.changeKeyboardMode(); break; case Keyboard.CODE_CANCEL: @@ -1157,10 +1166,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { - handleCharacter(primaryCode, keyCodes); + handleCharacter(primaryCode, keyCodes, x, y); } - // Cancel the just reverted state - mJustReverted = false; } switcher.onKey(primaryCode); // Reset after any single keystroke @@ -1172,7 +1179,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mVoiceConnector.commitVoiceInput(); InputConnection ic = getCurrentInputConnection(); if (ic == null) return; - abortCorrection(false); + abortRecorrection(false); ic.beginBatchEdit(); commitTyped(ic); maybeRemovePreviousPeriod(text); @@ -1180,7 +1187,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); - mJustReverted = false; mJustAddedAutoSpace = false; mEnteredText = text; } @@ -1220,7 +1226,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postUpdateShiftKeyState(); TextEntryState.backspace(); - if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { + if (TextEntryState.isUndoCommit()) { revertLastWord(deleteChar); ic.endBatchEdit(); return; @@ -1245,7 +1251,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - mJustReverted = false; ic.endBatchEdit(); } @@ -1273,20 +1278,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void abortCorrection(boolean force) { - if (force || TextEntryState.isCorrecting()) { - TextEntryState.onAbortCorrection(); + private void abortRecorrection(boolean force) { + if (force || TextEntryState.isRecorrecting()) { + TextEntryState.onAbortRecorrection(); setCandidatesViewShown(isCandidateStripVisible()); getCurrentInputConnection().finishComposingText(); clearSuggestions(); } } - private void handleCharacter(int primaryCode, int[] keyCodes) { + private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) { mVoiceConnector.handleCharacter(); - if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) { - abortCorrection(false); + if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isRecorrecting()) { + abortRecorrection(false); } int code = primaryCode; @@ -1296,6 +1301,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mComposing.setLength(0); saveWordInHistory(mBestWord); mWord.reset(); + clearSuggestions(); } } KeyboardSwitcher switcher = mKeyboardSwitcher; @@ -1323,7 +1329,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWord.setFirstCharCapitalized(true); } mComposing.append((char) code); - mWord.add(code, keyCodes); + mWord.add(code, keyCodes, x, y); InputConnection ic = getCurrentInputConnection(); if (ic != null) { // If it's the first letter, make note of auto-caps state @@ -1354,14 +1360,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); - abortCorrection(false); + abortRecorrection(false); } if (mHasValidSuggestions) { // 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. - if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) { + if (mAutoCorrectOn && primaryCode != '\'') { pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". @@ -1380,14 +1386,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Handle the case of ". ." -> " .." with auto-space if necessary // before changing the TextEntryState. - if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED - && primaryCode == Keyboard.CODE_PERIOD) { + if (TextEntryState.isPunctuationAfterAccepted() && primaryCode == Keyboard.CODE_PERIOD) { reswapPeriodAndSpace(); } TextEntryState.typedCharacter((char) primaryCode, true); - if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED - && primaryCode != Keyboard.CODE_ENTER) { + if (TextEntryState.isPunctuationAfterAccepted() && primaryCode != Keyboard.CODE_ENTER) { swapPunctuationAndSpace(); } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { doubleSpace(); @@ -1419,12 +1423,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); if (inputView != null) inputView.closing(); - TextEntryState.endSession(); } private void saveWordInHistory(CharSequence result) { if (mWord.size() <= 1) { - mWord.reset(); return; } // Skip if result is null. It happens in some edge case. @@ -1457,7 +1459,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean isCandidateStripVisible() { if (mCandidateView == null) return false; - if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isCorrecting()) + if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting()) return true; if (!isShowingSuggestionsStrip()) return false; @@ -1467,26 +1469,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void switchToKeyboardView() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (DEBUG) { - Log.d(TAG, "Switch to keyboard view."); - } - View v = mKeyboardSwitcher.getInputView(); - if (v != null) { - // Confirms that the keyboard view doesn't have parent view. - ViewParent p = v.getParent(); - if (p != null && p instanceof ViewGroup) { - ((ViewGroup) p).removeView(v); - } - setInputView(v); - } - setCandidatesViewShown(isCandidateStripVisible()); - updateInputViewShown(); - mHandler.postUpdateSuggestions(); + if (DEBUG) { + Log.d(TAG, "Switch to keyboard view."); + } + View v = mKeyboardSwitcher.getInputView(); + if (v != null) { + // Confirms that the keyboard view doesn't have parent view. + ViewParent p = v.getParent(); + if (p != null && p instanceof ViewGroup) { + ((ViewGroup) p).removeView(v); } - }); + setInputView(v); + } + setCandidatesViewShown(isCandidateStripVisible()); + updateInputViewShown(); + mHandler.postUpdateSuggestions(); } public void clearSuggestions() { @@ -1508,8 +1505,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void updateSuggestions() { - mKeyboardSwitcher.setPreferredLetters(null); - // Check if we have a suggestion engine attached. if ((mSuggest == null || !isSuggestionsRequested()) && !mVoiceConnector.isVoiceInputHighlighted()) { @@ -1528,36 +1523,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void showCorrections(WordAlternatives alternatives) { - mKeyboardSwitcher.setPreferredLetters(null); SuggestedWords.Builder builder = alternatives.getAlternatives(); builder.setTypedWordValid(false).setHasMinimalSuggestion(false); showSuggestions(builder.build(), alternatives.getOriginalWord()); } private void showSuggestions(WordComposer word) { - // TODO Maybe need better way of retrieving previous word + // TODO: May need a better way of retrieving previous word CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), mWordSeparators); SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( mKeyboardSwitcher.getInputView(), word, prevWord); - int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); - mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies); - - boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted - && mSuggest.hasAutoCorrection(); + boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection(); final CharSequence typedWord = word.getTypedWord(); - // If we're in basic correct - final boolean typedWordValid = mSuggest.isValidWord(typedWord) || - (preferCapitalization() - && mSuggest.isValidWord(typedWord.toString().toLowerCase())); + // Here, we want to promote a whitelisted word if exists. + final boolean typedWordValid = AutoCorrection.isValidWordForAutoCorrection( + mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization()); if (mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { correctionAvailable |= typedWordValid; } // Don't auto-correct words with multiple capital letter correctionAvailable &= !word.isMostlyCaps(); - correctionAvailable &= !TextEntryState.isCorrecting(); + correctionAvailable &= !TextEntryState.isRecorrecting(); // 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 @@ -1604,7 +1593,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mJustAccepted = true; pickSuggestion(mBestWord); // Add the word to the auto dictionary if it's not a known word - addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); + addToAutoAndUserBigramDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); return true; } @@ -1615,7 +1604,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen SuggestedWords suggestions = mCandidateView.getSuggestions(); mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); - final boolean correcting = TextEntryState.isCorrecting(); + final boolean recorrecting = TextEntryState.isRecorrecting(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); @@ -1657,24 +1646,35 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen pickSuggestion(suggestion); // Add the word to the auto dictionary if it's not a known word if (index == 0) { - addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); + addToAutoAndUserBigramDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); } else { - addToBigramDictionary(suggestion, 1); + addToOnlyBigramDictionary(suggestion, 1); } LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), index, suggestions.mWords); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space - if (mAutoSpace && !correcting) { + if (mAutoSpace && !recorrecting) { sendSpace(); mJustAddedAutoSpace = true; } - final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0 - && !mSuggest.isValidWord(suggestion) - && !mSuggest.isValidWord(suggestion.toString().toLowerCase()); - - if (!correcting) { + // We should show the hint if the user pressed the first entry AND either: + // - There is no dictionary (we know that because we tried to load it => null != mSuggest + // AND mHasDictionary is false) + // - There is a dictionary and the word is not in it + // Please note that if mSuggest is null, it means that everything is off: suggestion + // and correction, so we shouldn't try to show the hint + // We used to look at mCorrectionMode here, but showing the hint should have nothing + // to do with the autocorrection setting. + final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null + // If there is no dictionary the hint should be shown. + && (!mHasDictionary + // If "suggestion" is not in the dictionary, the hint should be shown. + || !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. @@ -1712,7 +1712,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen saveWordInHistory(suggestion); mHasValidSuggestions = false; mCommittedLength = suggestion.length(); - switcher.setPreferredLetters(null); } /** @@ -1725,6 +1724,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // If we didn't find a match, search for result in typed word history WordComposer foundWord = null; WordAlternatives alternatives = null; + // Search old suggestions to suggest re-corrected suggestions. for (WordAlternatives entry : mWordHistory) { if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) { if (entry instanceof TypedWordAlternatives) { @@ -1734,15 +1734,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen break; } } - // If we didn't find a match, at least suggest corrections. + // If we didn't find a match, at least suggest corrections as re-corrected suggestions. if (foundWord == null - && (mSuggest.isValidWord(touching.mWord) - || mSuggest.isValidWord(touching.mWord.toString().toLowerCase()))) { + && (AutoCorrection.isValidWord( + mSuggest.getUnigramDictionaries(), touching.mWord, true))) { foundWord = new WordComposer(); for (int i = 0; i < touching.mWord.length(); i++) { foundWord.add(touching.mWord.charAt(i), new int[] { touching.mWord.charAt(i) - }); + }, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); } foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0))); } @@ -1779,19 +1779,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!mVoiceConnector.applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) { - abortCorrection(true); + abortRecorrection(true); } else { - TextEntryState.selectedForCorrection(); + TextEntryState.selectedForRecorrection(); EditingUtils.underlineWord(ic, touching); } ic.endBatchEdit(); } else { - abortCorrection(true); + abortRecorrection(true); setPunctuationSuggestions(); // Show the punctuation suggestions list } } else { - abortCorrection(true); + abortRecorrection(true); } } @@ -1800,21 +1800,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setCandidatesViewShown(isCandidateStripVisible()); } - private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { + private void addToAutoAndUserBigramDictionaries(CharSequence suggestion, int frequencyDelta) { checkAddToDictionary(suggestion, frequencyDelta, false); } - private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) { + private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) { checkAddToDictionary(suggestion, frequencyDelta, true); } /** * Adds to the UserBigramDictionary and/or AutoDictionary - * @param addToBigramDictionary true if it should be added to bigram dictionary if possible + * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible */ private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, - boolean addToBigramDictionary) { + boolean selectedANotTypedWord) { if (suggestion == null || suggestion.length() < 1) return; + // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be // adding words in situations where the user or application really didn't // want corrections enabled or learned. @@ -1822,9 +1823,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { return; } - if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) - || (!mSuggest.isValidWord(suggestion.toString()) - && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { + + final boolean selectedATypedWordAndItsInAutoDic = + !selectedANotTypedWord && mAutoDictionary.isValidWord(suggestion); + final boolean isValidWord = AutoCorrection.isValidWord( + mSuggest.getUnigramDictionaries(), suggestion, true); + final boolean needsToAddToAutoDictionary = selectedATypedWordAndItsInAutoDic + || !isValidWord; + if (needsToAddToAutoDictionary) { mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); } @@ -1864,7 +1870,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int length = mComposing.length(); if (!mHasValidSuggestions && length > 0) { final InputConnection ic = getCurrentInputConnection(); - mJustReverted = true; final CharSequence punctuation = ic.getTextBeforeCursor(1, 0); if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; @@ -1892,7 +1897,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); - mJustReverted = false; } } @@ -1930,28 +1934,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeSwitcher.toggleLanguage(reset, next); } // Reload keyboard because the current language has been changed. - KeyboardSwitcher switcher = mKeyboardSwitcher; - final EditorInfo attribute = getCurrentInputEditorInfo(); - final int mode = initializeInputAttributesAndGetMode((attribute != null) - ? attribute.inputType : 0); - final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; - switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(), + mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), + mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceConnector.isVoiceButtonEnabled(), mVoiceConnector.isVoiceButtonOnPrimary()); initSuggest(); - switcher.updateShiftState(); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key); - if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { - mRefreshKeyboardRequired = true; - } else if (Settings.PREF_RECORRECTION_ENABLED.equals(key)) { - mReCorrectionEnabled = sharedPreferences.getBoolean( - Settings.PREF_RECORRECTION_ENABLED, - mResources.getBoolean(R.bool.default_recorrection_enabled)); - } + mKeyboardSwitcher.updateShiftState(); } @Override @@ -1961,7 +1948,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void onPress(int primaryCode) { + public void onPress(int primaryCode, boolean withSliding) { if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) { vibrate(); playKeyClick(primaryCode); @@ -1969,25 +1956,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyboardSwitcher switcher = mKeyboardSwitcher; final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { - switcher.onPressShift(); + switcher.onPressShift(withSliding); } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { switcher.onPressSymbol(); } else { switcher.onOtherKeyPressed(); } + mAccessibilityUtils.onPress(primaryCode, switcher); } @Override - public void onRelease(int primaryCode) { + public void onRelease(int primaryCode, boolean withSliding) { KeyboardSwitcher switcher = mKeyboardSwitcher; // Reset any drag flags in the keyboard switcher.keyReleased(); final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { - switcher.onReleaseShift(); + switcher.onReleaseShift(withSliding); } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { switcher.onReleaseSymbol(); } + mAccessibilityUtils.onRelease(primaryCode, switcher); } @@ -2067,6 +2056,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void updateCorrectionMode() { + // TODO: cleanup messy flags mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) && !mInputTypeNoAutoCorrect && mHasDictionary; @@ -2082,7 +2072,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void updateAutoTextEnabled() { if (mSuggest == null) return; - mSuggest.setAutoTextEnabled(mQuickFixes + mSuggest.setQuickFixesEnabled(mQuickFixes && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); } @@ -2121,7 +2111,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); mVibrateOn = vibrator != null && vibrator.hasVibrator() && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false); - mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, false); + mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, + mResources.getBoolean(R.bool.config_default_sound_enabled)); mPopupOn = isPopupEnabled(prefs); mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); @@ -2272,10 +2263,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen di.dismiss(); switch (position) { case 0: - launchSettings(); + mImm.showInputMethodPicker(); break; case 1: - mImm.showInputMethodPicker(); + launchSettings(); break; } } @@ -2285,6 +2276,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void showOptionsMenuInternal(CharSequence title, CharSequence[] items, DialogInterface.OnClickListener listener) { + final IBinder windowToken = mKeyboardSwitcher.getInputView().getWindowToken(); + if (windowToken == null) return; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); builder.setIcon(R.drawable.ic_dialog_keyboard); @@ -2295,7 +2288,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mOptionsDialog.setCanceledOnTouchOutside(true); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); + lp.token = windowToken; lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 12338ce61..341d5add0 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -225,7 +225,9 @@ public class Settings extends PreferenceActivity final String action; if (android.os.Build.VERSION.SDK_INT >= /* android.os.Build.VERSION_CODES.HONEYCOMB */ 11) { - action = "android.settings.INPUT_METHOD_AND_SUBTYPE_ENABLER"; + // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS + // TODO: Can this be a constant instead of literal String constant? + action = "android.settings.INPUT_METHOD_SUBTYPE_SETTINGS"; } else { action = "com.android.inputmethod.latin.INPUT_LANGUAGE_SELECTION"; } diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index f4262cc99..dc14d770a 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.LatinKeyboard; -import com.android.inputmethod.keyboard.LatinKeyboardView; import com.android.inputmethod.voice.SettingsUtil; import com.android.inputmethod.voice.VoiceIMEConnector; import com.android.inputmethod.voice.VoiceInput; @@ -47,7 +46,7 @@ import java.util.Map; public class SubtypeSwitcher { private static boolean DBG = LatinImeLogger.sDBG; - private static final String TAG = "SubtypeSwitcher"; + private static final String TAG = SubtypeSwitcher.class.getSimpleName(); private static final char LOCALE_SEPARATER = '_'; private static final String KEYBOARD_MODE = "keyboard"; @@ -75,10 +74,10 @@ public class SubtypeSwitcher { private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod; + private InputMethodSubtype mCurrentSubtype; private Locale mSystemLocale; private Locale mInputLocale; private String mInputLocaleStr; - private String mMode; private VoiceInput mVoiceInput; /*-----------------------------------------------------------*/ @@ -111,8 +110,7 @@ public class SubtypeSwitcher { mSystemLocale = null; mInputLocale = null; mInputLocaleStr = null; - // Mode is initialized to KEYBOARD_MODE, in case that LatinIME can't obtain currentSubtype - mMode = KEYBOARD_MODE; + mCurrentSubtype = null; mAllEnabledSubtypesOfCurrentInputMethod = null; // TODO: Voice input should be created here mVoiceInput = null; @@ -146,6 +144,7 @@ public class SubtypeSwitcher { // Reload enabledSubtypes from the framework. private void updateEnabledSubtypes() { + final String currentMode = getCurrentSubtypeMode(); boolean foundCurrentSubtypeBecameDisabled = true; mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList( null, true); @@ -158,7 +157,7 @@ public class SubtypeSwitcher { if (mLocaleSplitter.hasNext()) { mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); } - if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) { + if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) { foundCurrentSubtypeBecameDisabled = false; } if (KEYBOARD_MODE.equals(ims.getMode())) { @@ -169,7 +168,7 @@ public class SubtypeSwitcher { && mIsSystemLanguageSameAsInputLanguage); if (foundCurrentSubtypeBecameDisabled) { if (DBG) { - Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + mMode); + Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode); Log.w(TAG, "Last subtype was disabled. Update to the current one."); } updateSubtype(mImm.getCurrentInputMethodSubtype()); @@ -210,9 +209,10 @@ public class SubtypeSwitcher { public void updateSubtype(InputMethodSubtype newSubtype) { final String newLocale; final String newMode; + final String oldMode = getCurrentSubtypeMode(); if (newSubtype == null) { // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, - // fallback to the default locale and mode. + // fallback to the default locale. Log.w(TAG, "Couldn't get the current subtype."); newLocale = "en_US"; newMode = KEYBOARD_MODE; @@ -222,7 +222,7 @@ public class SubtypeSwitcher { } if (DBG) { Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode - + ", from: " + mInputLocaleStr + ", " + mMode); + + ", from: " + mInputLocaleStr + ", " + oldMode); } boolean languageChanged = false; if (!newLocale.equals(mInputLocaleStr)) { @@ -232,13 +232,12 @@ public class SubtypeSwitcher { updateInputLocale(newLocale); } boolean modeChanged = false; - String oldMode = mMode; - if (!newMode.equals(mMode)) { - if (mMode != null) { + if (!newMode.equals(oldMode)) { + if (oldMode != null) { modeChanged = true; } - mMode = newMode; } + mCurrentSubtype = newSubtype; // If the old mode is voice input, we need to reset or cancel its status. // We cancel its status when we change mode, while we reset otherwise. @@ -263,7 +262,7 @@ public class SubtypeSwitcher { triggerVoiceIME(); } } else { - Log.w(TAG, "Unknown subtype mode: " + mMode); + Log.w(TAG, "Unknown subtype mode: " + newMode); if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { // We need to reset the voice input to release the resources and to reset its status // as it is not the current input mode. @@ -356,11 +355,27 @@ public class SubtypeSwitcher { return false; } - public boolean isShortcutAvailable() { + public boolean isShortcutImeEnabled() { if (mShortcutInputMethodInfo == null) return false; - if (mShortcutSubtype != null && contains(mShortcutSubtype.getExtraValue().split(","), - SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) { + if (mShortcutSubtype == null) + return true; + final boolean allowsImplicitlySelectedSubtypes = true; + for (final InputMethodSubtype enabledSubtype : mImm.getEnabledInputMethodSubtypeList( + mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) { + if (enabledSubtype.equals(mShortcutSubtype)) + return true; + } + return false; + } + + public boolean isShortcutImeReady() { + if (mShortcutInputMethodInfo == null) + return false; + if (mShortcutSubtype == null) + return true; + if (contains(mShortcutSubtype.getExtraValue().split(","), + SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) { return mIsNetworkConnected; } return true; @@ -371,12 +386,10 @@ public class SubtypeSwitcher { ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); mIsNetworkConnected = !noConnection; - final LatinKeyboardView inputView = KeyboardSwitcher.getInstance().getInputView(); - if (inputView != null) { - final LatinKeyboard keyboard = inputView.getLatinKeyboard(); - if (keyboard != null) { - keyboard.updateShortcutKey(isShortcutAvailable(), inputView); - } + final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); + final LatinKeyboard keyboard = switcher.getLatinKeyboard(); + if (keyboard != null) { + keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getInputView()); } } @@ -426,8 +439,15 @@ public class SubtypeSwitcher { if (mConfigUseSpacebarLanguageSwitcher) { return mLanguageSwitcher.getEnabledLanguages(); } else { + int enabledLanguageCount = mEnabledLanguagesOfCurrentInputMethod.size(); + // Workaround for explicitly specifying the voice language + if (enabledLanguageCount == 1) { + mEnabledLanguagesOfCurrentInputMethod.add( + mEnabledLanguagesOfCurrentInputMethod.get(0)); + ++enabledLanguageCount; + } return mEnabledLanguagesOfCurrentInputMethod.toArray( - new String[mEnabledLanguagesOfCurrentInputMethod.size()]); + new String[enabledLanguageCount]); } } @@ -465,14 +485,6 @@ public class SubtypeSwitcher { } } - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (mConfigUseSpacebarLanguageSwitcher) { - if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { - mLanguageSwitcher.loadLocales(sharedPreferences); - } - } - } - /** * Change system locale for this application * @param newLocale @@ -487,7 +499,7 @@ public class SubtypeSwitcher { } public boolean isKeyboardMode() { - return KEYBOARD_MODE.equals(mMode); + return KEYBOARD_MODE.equals(getCurrentSubtypeMode()); } @@ -510,7 +522,7 @@ public class SubtypeSwitcher { } public boolean isVoiceMode() { - return VOICE_MODE.equals(mMode); + return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode()); } private void triggerVoiceIME() { @@ -576,6 +588,30 @@ public class SubtypeSwitcher { } } + ///////////////////////////// + // Other utility functions // + ///////////////////////////// + + public String getCurrentSubtypeExtraValue() { + // If null, return what an empty ExtraValue would return : the empty string. + return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : ""; + } + + public boolean currentSubtypeContainsExtraValueKey(String key) { + // If null, return what an empty ExtraValue would return : false. + return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false; + } + + public String getCurrentSubtypeExtraValueOf(String key) { + // If null, return what an empty ExtraValue would return : null. + return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null; + } + + public String getCurrentSubtypeMode() { + return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE; + } + + // A list of locales which are supported by default for voice input, unless we get a // different list from Gservices. private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index ced355bb2..0de474e59 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -22,8 +22,13 @@ import android.text.TextUtils; import android.util.Log; import android.view.View; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * This class loads a dictionary and provides a list of suggestions for a given sequence of @@ -62,40 +67,37 @@ public class Suggest implements Dictionary.WordCallback { // If you add a type of dictionary, increment DIC_TYPE_LAST_ID public static final int DIC_TYPE_LAST_ID = 4; - static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; - - private static boolean DBG = LatinImeLogger.sDBG; - - private BinaryDictionary mMainDict; + public static final String DICT_KEY_MAIN = "main"; + public static final String DICT_KEY_CONTACTS = "contacts"; + public static final String DICT_KEY_AUTO = "auto"; + public static final String DICT_KEY_USER = "user"; + public static final String DICT_KEY_USER_BIGRAM = "user_bigram"; + public static final String DICT_KEY_WHITELIST ="whitelist"; - private Dictionary mUserDictionary; + static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; - private Dictionary mAutoDictionary; + private static final boolean DBG = LatinImeLogger.sDBG; - private Dictionary mContactsDictionary; + private AutoCorrection mAutoCorrection; - private Dictionary mUserBigramDictionary; + private BinaryDictionary mMainDict; + private WhitelistDictionary mWhiteListDictionary; + private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>(); + private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>(); private int mPrefMaxSuggestions = 12; private static final int PREF_MAX_BIGRAMS = 60; - private boolean mAutoTextEnabled; + private boolean mQuickFixesEnabled; private double mAutoCorrectionThreshold; private int[] mPriorities = new int[mPrefMaxSuggestions]; private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; - // Handle predictive correction for only the first 1280 characters for performance reasons - // If we support scripts that need latin characters beyond that, we should probably use some - // kind of a sparse array or language specific list with a mapping lookup table. - // 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of - // latin characters. - private int[] mNextLettersFrequencies = new int[1280]; private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>(); private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); - private boolean mHasAutoCorrection; private String mLowerOriginalWord; // TODO: Remove these member variables by passing more context to addWord() callback method @@ -105,7 +107,24 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; public Suggest(Context context, int dictionaryResId) { - mMainDict = BinaryDictionary.initDictionary(context, dictionaryResId, DIC_MAIN); + init(context, BinaryDictionary.initDictionary(context, dictionaryResId, DIC_MAIN)); + } + + /* package for test */ Suggest(File dictionary, long startOffset, long length) { + init(null, BinaryDictionary.initDictionary(dictionary, startOffset, length, DIC_MAIN)); + } + + private void init(Context context, BinaryDictionary mainDict) { + if (mainDict != null) { + mMainDict = mainDict; + mUnigramDictionaries.put(DICT_KEY_MAIN, mainDict); + mBigramDictionaries.put(DICT_KEY_MAIN, mainDict); + } + mWhiteListDictionary = WhitelistDictionary.init(context); + if (mWhiteListDictionary != null) { + mUnigramDictionaries.put(DICT_KEY_WHITELIST, mWhiteListDictionary); + } + mAutoCorrection = new AutoCorrection(); initPool(); } @@ -116,8 +135,8 @@ public class Suggest implements Dictionary.WordCallback { } } - public void setAutoTextEnabled(boolean enabled) { - mAutoTextEnabled = enabled; + public void setQuickFixesEnabled(boolean enabled) { + mQuickFixesEnabled = enabled; } public int getCorrectionMode() { @@ -132,6 +151,10 @@ public class Suggest implements Dictionary.WordCallback { return mMainDict != null && mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; } + public Map<String, Dictionary> getUnigramDictionaries() { + return mUnigramDictionaries; + } + public int getApproxMaxWordLength() { return APPROX_MAX_WORD_LENGTH; } @@ -141,22 +164,28 @@ public class Suggest implements Dictionary.WordCallback { * before the main dictionary, if set. */ public void setUserDictionary(Dictionary userDictionary) { - mUserDictionary = userDictionary; + if (userDictionary != null) + mUnigramDictionaries.put(DICT_KEY_USER, userDictionary); } /** * Sets an optional contacts dictionary resource to be loaded. */ - public void setContactsDictionary(Dictionary userDictionary) { - mContactsDictionary = userDictionary; + public void setContactsDictionary(Dictionary contactsDictionary) { + if (contactsDictionary != null) { + mUnigramDictionaries.put(DICT_KEY_CONTACTS, contactsDictionary); + mBigramDictionaries.put(DICT_KEY_CONTACTS, contactsDictionary); + } } public void setAutoDictionary(Dictionary autoDictionary) { - mAutoDictionary = autoDictionary; + if (autoDictionary != null) + mUnigramDictionaries.put(DICT_KEY_AUTO, autoDictionary); } public void setUserBigramDictionary(Dictionary userBigramDictionary) { - mUserBigramDictionary = userBigramDictionary; + if (userBigramDictionary != null) + mBigramDictionaries.put(DICT_KEY_USER_BIGRAM, userBigramDictionary); } public void setAutoCorrectionThreshold(double threshold) { @@ -200,16 +229,34 @@ public class Suggest implements Dictionary.WordCallback { return getSuggestedWordBuilder(view, wordComposer, prevWordForBigram).build(); } + private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) { + if (TextUtils.isEmpty(word) || !(all || first)) return word; + final int wordLength = word.length(); + final int poolSize = mStringPool.size(); + final StringBuilder sb = + poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) + : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + if (all) { + sb.append(word.toString().toUpperCase()); + } else if (first) { + sb.append(Character.toUpperCase(word.charAt(0))); + if (wordLength > 1) { + sb.append(word.subSequence(1, wordLength)); + } + } + return sb; + } + // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer, CharSequence prevWordForBigram) { LatinImeLogger.onStartSuggestion(prevWordForBigram); - mHasAutoCorrection = false; + mAutoCorrection.init(); mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); collectGarbage(mSuggestions, mPrefMaxSuggestions); Arrays.fill(mPriorities, 0); - Arrays.fill(mNextLettersFrequencies, 0); // Save a lowercase version of the original word CharSequence typedWord = wordComposer.getTypedWord(); @@ -235,17 +282,8 @@ public class Suggest implements Dictionary.WordCallback { if (mMainDict != null && mMainDict.isValidWord(lowerPrevWord)) { prevWordForBigram = lowerPrevWord; } - if (mUserBigramDictionary != null) { - mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this, - mNextLettersFrequencies); - } - if (mContactsDictionary != null) { - mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this, - mNextLettersFrequencies); - } - if (mMainDict != null) { - mMainDict.getBigrams(wordComposer, prevWordForBigram, this, - mNextLettersFrequencies); + for (final Dictionary dictionary : mBigramDictionaries.values()) { + dictionary.getBigrams(wordComposer, prevWordForBigram, this); } char currentChar = wordComposer.getTypedWord().charAt(0); char currentCharUpper = Character.toUpperCase(currentChar); @@ -268,97 +306,86 @@ public class Suggest implements Dictionary.WordCallback { } else if (wordComposer.size() > 1) { // At second character typed, search the unigrams (scores being affected by bigrams) - if (mUserDictionary != null || mContactsDictionary != null) { - if (mUserDictionary != null) { - mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies); - } - if (mContactsDictionary != null) { - mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies); - } - - if (mSuggestions.size() > 0 && isValidWord(typedWord) - && (mCorrectionMode == CORRECTION_FULL - || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { - if (DBG) { - Log.d(TAG, "Auto corrected by CORRECTION_FULL."); - } - mHasAutoCorrection = true; - } - } - if (mMainDict != null) mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); - if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM) - && mSuggestions.size() > 0 && mPriorities.length > 0) { - // TODO: when the normalized score of the first suggestion is nearly equals to - // the normalized score of the second suggestion, behave less aggressive. - final double normalizedScore = Utils.calcNormalizedScore( - typedWord, mSuggestions.get(0), mPriorities[0]); - if (LatinImeLogger.sDBG) { - Log.d(TAG, "Normalized " + typedWord + "," + mSuggestions.get(0) + "," - + mPriorities[0] + ", " + normalizedScore - + "(" + mAutoCorrectionThreshold + ")"); - } - if (normalizedScore >= mAutoCorrectionThreshold) { - if (DBG) { - Log.d(TAG, "Auto corrected by S-threthhold."); - } - mHasAutoCorrection = true; - } + for (final String key : mUnigramDictionaries.keySet()) { + // Skip AutoDictionary and WhitelistDictionary to lookup + if (key.equals(DICT_KEY_AUTO) || key.equals(DICT_KEY_WHITELIST)) + continue; + final Dictionary dictionary = mUnigramDictionaries.get(key); + dictionary.getWords(wordComposer, this); } } + CharSequence autoText = null; + final String typedWordString = typedWord == null ? null : typedWord.toString(); if (typedWord != null) { - mSuggestions.add(0, typedWord.toString()); - } - if (mAutoTextEnabled) { - int i = 0; - int max = 6; - // Don't autotext the suggestions from the dictionaries - if (mCorrectionMode == CORRECTION_BASIC) max = 1; - while (i < mSuggestions.size() && i < max) { - String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); - CharSequence autoText = - AutoText.get(suggestedWord, 0, suggestedWord.length(), view); + // Apply quick fix only for the typed word. + if (mQuickFixesEnabled) { + final String lowerCaseTypedWord = typedWordString.toLowerCase(); + CharSequence tempAutoText = capitalizeWord( + mIsAllUpperCase, mIsFirstCharCapitalized, AutoText.get( + lowerCaseTypedWord, 0, lowerCaseTypedWord.length(), view)); + // TODO: cleanup canAdd // Is there an AutoText (also known as Quick Fixes) correction? - boolean canAdd = autoText != null; // Capitalize as needed - final int autoTextLength = autoText != null ? autoText.length() : 0; - if (autoTextLength > 0 && (mIsAllUpperCase || mIsFirstCharCapitalized)) { - int poolSize = mStringPool.size(); - StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove( - poolSize - 1) : new StringBuilder(getApproxMaxWordLength()); - sb.setLength(0); - if (mIsAllUpperCase) { - sb.append(autoText.toString().toUpperCase()); - } else if (mIsFirstCharCapitalized) { - sb.append(Character.toUpperCase(autoText.charAt(0))); - if (autoTextLength > 1) { - sb.append(autoText.subSequence(1, autoTextLength)); - } - } - autoText = sb.toString(); - } + boolean canAdd = tempAutoText != null; // Is that correction already the current prediction (or original word)? - canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i)); + canAdd &= !TextUtils.equals(tempAutoText, typedWord); // Is that correction already the next predicted word? - if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) { - canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1)); + if (canAdd && mSuggestions.size() > 0 && mCorrectionMode != CORRECTION_BASIC) { + canAdd &= !TextUtils.equals(tempAutoText, mSuggestions.get(0)); } if (canAdd) { if (DBG) { Log.d(TAG, "Auto corrected by AUTOTEXT."); } - mHasAutoCorrection = true; - mSuggestions.add(i + 1, autoText); - i++; + autoText = tempAutoText; } - i++; } } + + CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized, + mWhiteListDictionary.getWhiteListedWord(typedWordString)); + + mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer, + mSuggestions, mPriorities, typedWord, mAutoCorrectionThreshold, mCorrectionMode, + autoText, whitelistedWord); + + if (autoText != null) { + mSuggestions.add(0, autoText); + } + + if (whitelistedWord != null) { + mSuggestions.add(0, whitelistedWord); + } + + if (typedWord != null) { + mSuggestions.add(0, typedWordString); + } removeDupes(); - return new SuggestedWords.Builder().addWords(mSuggestions, null); - } - public int[] getNextLettersFrequencies() { - return mNextLettersFrequencies; + if (DBG) { + double normalizedScore = mAutoCorrection.getNormalizedScore(); + ArrayList<SuggestedWords.SuggestedWordInfo> frequencyInfoList = + new ArrayList<SuggestedWords.SuggestedWordInfo>(); + frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo("+", false)); + final int priorityLength = mPriorities.length; + for (int i = 0; i < priorityLength; ++i) { + if (normalizedScore > 0) { + final String priorityThreshold = Integer.toString(mPriorities[i]) + " (" + + normalizedScore + ")"; + frequencyInfoList.add( + new SuggestedWords.SuggestedWordInfo(priorityThreshold, false)); + normalizedScore = 0.0; + } else { + final String priority = Integer.toString(mPriorities[i]); + frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo(priority, false)); + } + } + for (int i = priorityLength; i < mSuggestions.size(); ++i) { + frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo("--", false)); + } + return new SuggestedWords.Builder().addWords(mSuggestions, frequencyInfoList); + } + return new SuggestedWords.Builder().addWords(mSuggestions, null); } private void removeDupes() { @@ -389,15 +416,15 @@ public class Suggest implements Dictionary.WordCallback { } public boolean hasAutoCorrection() { - return mHasAutoCorrection; + return mAutoCorrection.hasAutoCorrection(); } - private boolean compareCaseInsensitive(final String mLowerOriginalWord, + private static boolean compareCaseInsensitive(final String lowerOriginalWord, final char[] word, final int offset, final int length) { - final int originalLength = mLowerOriginalWord.length(); + final int originalLength = lowerOriginalWord.length(); if (originalLength == length && Character.isUpperCase(word[offset])) { for (int i = 0; i < originalLength; i++) { - if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) { + if (lowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) { return false; } } @@ -427,7 +454,20 @@ public class Suggest implements Dictionary.WordCallback { // Check if it's the same word, only caps are different if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) { - pos = 0; + // TODO: remove this surrounding if clause and move this logic to + // getSuggestedWordBuilder. + if (suggestions.size() > 0) { + final String currentHighestWordLowerCase = + suggestions.get(0).toString().toLowerCase(); + // If the current highest word is also equal to typed word, we need to compare + // frequency to determine the insertion position. This does not ensure strictly + // correct ordering, but ensures the top score is on top which is enough for + // removing duplicates correctly. + if (compareCaseInsensitive(currentHighestWordLowerCase, word, offset, length) + && freq <= priorities[0]) { + pos = 1; + } + } } else { if (dataType == Dictionary.DataType.UNIGRAM) { // Check if the word was already added before (by bigram data) @@ -510,16 +550,6 @@ public class Suggest implements Dictionary.WordCallback { return -1; } - public boolean isValidWord(final CharSequence word) { - if (word == null || word.length() == 0 || mMainDict == null) { - return false; - } - return mMainDict.isValidWord(word) - || (mUserDictionary != null && mUserDictionary.isValidWord(word)) - || (mAutoDictionary != null && mAutoDictionary.isValidWord(word)) - || (mContactsDictionary != null && mContactsDictionary.isValidWord(word)); - } - private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) { int poolSize = mStringPool.size(); int garbageSize = suggestions.size(); @@ -538,25 +568,12 @@ public class Suggest implements Dictionary.WordCallback { } public void close() { - if (mMainDict != null) { - mMainDict.close(); - mMainDict = null; - } - if (mUserDictionary != null) { - mUserDictionary.close(); - mUserDictionary = null; - } - if (mUserBigramDictionary != null) { - mUserBigramDictionary.close(); - mUserBigramDictionary = null; - } - if (mContactsDictionary != null) { - mContactsDictionary.close(); - mContactsDictionary = null; - } - if (mAutoDictionary != null) { - mAutoDictionary.close(); - mAutoDictionary = null; + final Set<Dictionary> dictionaries = new HashSet<Dictionary>(); + dictionaries.addAll(mUnigramDictionaries.values()); + dictionaries.addAll(mBigramDictionaries.values()); + for (final Dictionary dictionary : dictionaries) { + dictionary.close(); } + mMainDict = null; } } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index f774ce3a5..fe7aac7c2 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -24,23 +24,20 @@ import java.util.HashSet; import java.util.List; public class SuggestedWords { - public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, false, null); + public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, null); public final List<CharSequence> mWords; - public final boolean mIsApplicationSpecifiedCompletions; public final boolean mTypedWordValid; public final boolean mHasMinimalSuggestion; public final List<SuggestedWordInfo> mSuggestedWordInfoList; - private SuggestedWords(List<CharSequence> words, boolean isApplicationSpecifiedCompletions, - boolean typedWordValid, boolean hasMinamlSuggestion, - List<SuggestedWordInfo> suggestedWordInfoList) { + private SuggestedWords(List<CharSequence> words, boolean typedWordValid, + boolean hasMinamlSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) { if (words != null) { mWords = words; } else { mWords = Collections.emptyList(); } - mIsApplicationSpecifiedCompletions = isApplicationSpecifiedCompletions; mTypedWordValid = typedWordValid; mHasMinimalSuggestion = hasMinamlSuggestion; mSuggestedWordInfoList = suggestedWordInfoList; @@ -64,7 +61,6 @@ public class SuggestedWords { public static class Builder { private List<CharSequence> mWords = new ArrayList<CharSequence>(); - private boolean mIsCompletions; private boolean mTypedWordValid; private boolean mHasMinimalSuggestion; private List<SuggestedWordInfo> mSuggestedWordInfoList = @@ -109,7 +105,6 @@ public class SuggestedWords { public Builder setApplicationSpecifiedCompletions(CompletionInfo[] infos) { for (CompletionInfo info : infos) addWord(info.getText()); - mIsCompletions = true; return this; } @@ -141,15 +136,14 @@ public class SuggestedWords { alreadySeen.add(prevWord); } } - mIsCompletions = false; mTypedWordValid = false; mHasMinimalSuggestion = false; return this; } public SuggestedWords build() { - return new SuggestedWords(mWords, mIsCompletions, mTypedWordValid, - mHasMinimalSuggestion, mSuggestedWordInfoList); + return new SuggestedWords(mWords, mTypedWordValid, mHasMinimalSuggestion, + mSuggestedWordInfoList); } public int size() { diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index f571f26d5..63196430b 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -16,117 +16,39 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.keyboard.Key; - -import android.content.Context; -import android.text.format.DateFormat; import android.util.Log; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Calendar; - public class TextEntryState { - - private static final boolean DBG = false; - - private static final String TAG = "TextEntryState"; - - private static boolean LOGGING = false; - - private static int sBackspaceCount = 0; - - private static int sAutoSuggestCount = 0; - - private static int sAutoSuggestUndoneCount = 0; - - private static int sManualSuggestCount = 0; - - private static int sWordNotInDictionaryCount = 0; - - private static int sSessionCount = 0; - - private static int sTypedChars; - - private static int sActualChars; - - public enum State { - UNKNOWN, - START, - IN_WORD, - ACCEPTED_DEFAULT, - PICKED_SUGGESTION, - PUNCTUATION_AFTER_WORD, - PUNCTUATION_AFTER_ACCEPTED, - SPACE_AFTER_ACCEPTED, - SPACE_AFTER_PICKED, - UNDO_COMMIT, - CORRECTING, - PICKED_CORRECTION, + 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; } - private static State sState = State.UNKNOWN; - - private static FileOutputStream sKeyLocationFile; - private static FileOutputStream sUserActionFile; - - public static void newSession(Context context) { - sSessionCount++; - sAutoSuggestCount = 0; - sBackspaceCount = 0; - sAutoSuggestUndoneCount = 0; - sManualSuggestCount = 0; - sWordNotInDictionaryCount = 0; - sTypedChars = 0; - sActualChars = 0; - sState = State.START; - - if (LOGGING) { - try { - sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND); - sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND); - } catch (IOException ioe) { - Log.e("TextEntryState", "Couldn't open file for output: " + ioe); - } - } - } - - public static void endSession() { - if (sKeyLocationFile == null) { - return; - } - try { - sKeyLocationFile.close(); - // Write to log file - // Write timestamp, settings, - String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime()) - .toString() - + " BS: " + sBackspaceCount - + " auto: " + sAutoSuggestCount - + " manual: " + sManualSuggestCount - + " typed: " + sWordNotInDictionaryCount - + " undone: " + sAutoSuggestUndoneCount - + " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars) - + "\n"; - sUserActionFile.write(out.getBytes()); - sUserActionFile.close(); - sKeyLocationFile = null; - sUserActionFile = null; - } catch (IOException ioe) { - // ignore - } - } - public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) { if (typedWord == null) return; - if (!typedWord.equals(actualWord)) { - sAutoSuggestCount++; - } - sTypedChars += typedWord.length(); - sActualChars += actualWord.length(); - sState = State.ACCEPTED_DEFAULT; + setState(ACCEPTED_DEFAULT); LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString()); - displayState(); + if (DEBUG) + displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord); } // State.ACCEPTED_DEFAULT will be changed to other sub-states @@ -138,151 +60,167 @@ public class TextEntryState { case SPACE_AFTER_ACCEPTED: case PUNCTUATION_AFTER_ACCEPTED: case IN_WORD: - sState = State.ACCEPTED_DEFAULT; + setState(ACCEPTED_DEFAULT); break; default: break; } - displayState(); + if (DEBUG) displayState("backToAcceptedDefault", "typedWord", typedWord); } - public static void acceptedTyped(@SuppressWarnings("unused") CharSequence typedWord) { - sWordNotInDictionaryCount++; - sState = State.PICKED_SUGGESTION; - displayState(); + public static void acceptedTyped(CharSequence typedWord) { + setState(PICKED_SUGGESTION); + if (DEBUG) displayState("acceptedTyped", "typedWord", typedWord); } public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) { - sManualSuggestCount++; - State oldState = sState; - if (typedWord.equals(actualWord)) { - acceptedTyped(typedWord); - } - if (oldState == State.CORRECTING || oldState == State.PICKED_CORRECTION) { - sState = State.PICKED_CORRECTION; + if (sState == RECORRECTING || sState == PICKED_RECORRECTION) { + setState(PICKED_RECORRECTION); } else { - sState = State.PICKED_SUGGESTION; + setState(PICKED_SUGGESTION); } - displayState(); + if (DEBUG) + displayState("acceptedSuggestion", "typedWord", typedWord, "actualWord", actualWord); } - public static void selectedForCorrection() { - sState = State.CORRECTING; - displayState(); + public static void selectedForRecorrection() { + setState(RECORRECTING); + if (DEBUG) displayState("selectedForRecorrection"); } - public static void onAbortCorrection() { - if (isCorrecting()) { - sState = State.START; + public static void onAbortRecorrection() { + if (sState == RECORRECTING || sState == PICKED_RECORRECTION) { + setState(START); } - displayState(); + if (DEBUG) displayState("onAbortRecorrection"); } public static void typedCharacter(char c, boolean isSeparator) { - boolean isSpace = c == ' '; + final boolean isSpace = (c == ' '); switch (sState) { - case IN_WORD: - if (isSpace || isSeparator) { - sState = State.START; - } else { - // State hasn't changed. - } - break; - case ACCEPTED_DEFAULT: - case SPACE_AFTER_PICKED: - if (isSpace) { - sState = State.SPACE_AFTER_ACCEPTED; - } else if (isSeparator) { - sState = State.PUNCTUATION_AFTER_ACCEPTED; - } else { - sState = State.IN_WORD; - } - break; - case PICKED_SUGGESTION: - case PICKED_CORRECTION: - if (isSpace) { - sState = State.SPACE_AFTER_PICKED; - } else if (isSeparator) { - // Swap - sState = State.PUNCTUATION_AFTER_ACCEPTED; - } else { - sState = State.IN_WORD; - } - break; - case START: - case UNKNOWN: - case SPACE_AFTER_ACCEPTED: - case PUNCTUATION_AFTER_ACCEPTED: - case PUNCTUATION_AFTER_WORD: - if (!isSpace && !isSeparator) { - sState = State.IN_WORD; - } else { - sState = State.START; - } - break; - case UNDO_COMMIT: - if (isSpace || isSeparator) { - sState = State.ACCEPTED_DEFAULT; - } else { - sState = State.IN_WORD; - } - break; - case CORRECTING: - sState = State.START; - break; + 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(ACCEPTED_DEFAULT); + } else { + setState(IN_WORD); + } + break; + case RECORRECTING: + setState(START); + break; } - displayState(); + if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator); } public static void backspace() { - if (sState == State.ACCEPTED_DEFAULT) { - sState = State.UNDO_COMMIT; - sAutoSuggestUndoneCount++; + if (sState == ACCEPTED_DEFAULT) { + setState(UNDO_COMMIT); LatinImeLogger.logOnAutoSuggestionCanceled(); - } else if (sState == State.UNDO_COMMIT) { - sState = State.IN_WORD; + } else if (sState == UNDO_COMMIT) { + setState(IN_WORD); } - sBackspaceCount++; - displayState(); + if (DEBUG) displayState("backspace"); } public static void reset() { - sState = State.START; - displayState(); + setState(START); + if (DEBUG) displayState("reset"); } - public static State getState() { - if (DBG) { - Log.d(TAG, "Returning state = " + sState); - } - return sState; + public static boolean isAcceptedDefault() { + return sState == ACCEPTED_DEFAULT; } - public static boolean isCorrecting() { - return sState == State.CORRECTING || sState == State.PICKED_CORRECTION; + public static boolean isSpaceAfterPicked() { + return sState == SPACE_AFTER_PICKED; } - public static void keyPressedAt(Key key, int x, int y) { - if (LOGGING && sKeyLocationFile != null && key.mCode >= 32) { - String out = - "KEY: " + (char) key.mCode - + " X: " + x - + " Y: " + y - + " MX: " + (key.mX + key.mWidth / 2) - + " MY: " + (key.mY + key.mHeight / 2) - + "\n"; - try { - sKeyLocationFile.write(out.getBytes()); - } catch (IOException ioe) { - // TODO: May run out of space - } + 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() { - if (DBG) { - Log.d(TAG, "State = " + sState); + 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/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java index 56ee5b9e7..c06bd736e 100644 --- a/java/src/com/android/inputmethod/latin/UserDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserDictionary.java @@ -126,9 +126,8 @@ public class UserDictionary extends ExpandableDictionary { } @Override - public synchronized void getWords(final WordComposer codes, final WordCallback callback, - int[] nextLettersFrequencies) { - super.getWords(codes, callback, nextLettersFrequencies); + public synchronized void getWords(final WordComposer codes, final WordCallback callback) { + super.getWords(codes, callback); } @Override @@ -138,7 +137,7 @@ public class UserDictionary extends ExpandableDictionary { private void addWords(Cursor cursor) { clearDictionary(); - + if (cursor == null) return; final int maxWordLength = getMaxWordLength(); if (cursor.moveToFirst()) { final int indexWord = cursor.getColumnIndex(Words.WORD); diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index e980d3a30..727e3f16d 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -16,13 +16,18 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.keyboard.KeyboardId; + +import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.os.AsyncTask; import android.os.Handler; import android.os.HandlerThread; import android.os.Process; +import android.text.InputType; import android.text.format.DateUtils; import android.util.Log; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; @@ -41,6 +46,10 @@ public class Utils { private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4; private static boolean DBG = LatinImeLogger.sDBG; + private Utils() { + // Intentional empty constructor for utility class. + } + /** * Cancel an {@link AsyncTask}. * @@ -55,7 +64,7 @@ public class Utils { } public static class GCUtils { - private static final String TAG = "GCUtils"; + private static final String GC_TAG = GCUtils.class.getSimpleName(); public static final int GC_TRY_COUNT = 2; // GC_TRY_LOOP_MAX is used for the hard limit of GC wait, // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT. @@ -84,7 +93,7 @@ public class Utils { Thread.sleep(GC_INTERVAL); return true; } catch (InterruptedException e) { - Log.e(TAG, "Sleep was interrupted."); + Log.e(GC_TAG, "Sleep was interrupted."); LatinImeLogger.logOnException(metaData, t); return false; } @@ -261,6 +270,19 @@ public class Utils { return dp[sl][tl]; } + // Get the current stack trace + public static String getStackTrace() { + StringBuilder sb = new StringBuilder(); + try { + throw new RuntimeException(); + } catch (RuntimeException e) { + StackTraceElement[] frames = e.getStackTrace(); + // Start at 1 because the first frame is here and we don't care about it + for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n"); + } + return sb.toString(); + } + // In dictionary.cpp, getSuggestion() method, // suggestion scores are computed using the below formula. // original score (called 'frequency') @@ -268,13 +290,22 @@ public class Utils { // (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].) - // * (when before.length() == after.length(), - // mFullWordMultiplier (this is defined 2)) - // So, maximum original score is pow(2, before.length()) * 255 * 2 - // So, we can normalize original score by dividing this value. + // 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 frequency 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_MULTIPLYER = 2; + private static final int FULL_WORD_MULTIPLIER = 2; public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) { final int beforeLength = before.length(); final int afterLength = after.length(); @@ -284,7 +315,7 @@ public class Utils { // correction. final double maximumScore = MAX_INITIAL_SCORE * Math.pow(TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength)) - * FULL_WORD_MULTIPLYER; + * FULL_WORD_MULTIPLIER; // add a weight based on edit distance. // distance <= max(afterLength, beforeLength) == afterLength, // so, 0 <= distance / afterLength <= 1 @@ -293,7 +324,7 @@ public class Utils { } public static class UsabilityStudyLogUtils { - private static final String TAG = "UsabilityStudyLogUtils"; + private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); private static final String FILENAME = "log.txt"; private static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils(); @@ -330,7 +361,7 @@ public class Utils { try { mWriter = getPrintWriter(mDirectory, FILENAME, false); } catch (IOException e) { - Log.e(TAG, "Can't create log file."); + Log.e(USABILITY_TAG, "Can't create log file."); } } } @@ -367,7 +398,7 @@ public class Utils { final String printString = String.format("%s\t%d\t%s\n", mDateFormat.format(mDate), currentTime, log); if (LatinImeLogger.sDBG) { - Log.d(TAG, "Write: " + log); + Log.d(USABILITY_TAG, "Write: " + log); } mWriter.print(printString); } @@ -388,10 +419,10 @@ public class Utils { sb.append(line); } } catch (IOException e) { - Log.e(TAG, "Can't read log file."); + Log.e(USABILITY_TAG, "Can't read log file."); } finally { if (LatinImeLogger.sDBG) { - Log.d(TAG, "output all logs\n" + sb.toString()); + Log.d(USABILITY_TAG, "output all logs\n" + sb.toString()); } mIms.getCurrentInputConnection().commitText(sb.toString(), 0); try { @@ -410,7 +441,7 @@ public class Utils { public void run() { if (mFile != null && mFile.exists()) { if (LatinImeLogger.sDBG) { - Log.d(TAG, "Delete log file."); + Log.d(USABILITY_TAG, "Delete log file."); } mFile.delete(); mWriter.close(); @@ -439,4 +470,95 @@ public class Utils { return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */); } } + + public static int getKeyboardMode(EditorInfo attribute) { + if (attribute == null) + return KeyboardId.MODE_TEXT; + + final int inputType = attribute.inputType; + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + + switch (inputType & InputType.TYPE_MASK_CLASS) { + case InputType.TYPE_CLASS_NUMBER: + case InputType.TYPE_CLASS_DATETIME: + return KeyboardId.MODE_NUMBER; + case InputType.TYPE_CLASS_PHONE: + return KeyboardId.MODE_PHONE; + case InputType.TYPE_CLASS_TEXT: + if (Utils.isEmailVariation(variation)) { + return KeyboardId.MODE_EMAIL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { + return KeyboardId.MODE_URL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + return KeyboardId.MODE_IM; + } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return KeyboardId.MODE_TEXT; + } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { + return KeyboardId.MODE_WEB; + } else { + return KeyboardId.MODE_TEXT; + } + default: + return KeyboardId.MODE_TEXT; + } + } + + public static boolean isEmailVariation(int variation) { + return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; + } + + // Please refer to TextView.isPasswordInputType + public static boolean isPasswordInputType(int inputType) { + final int variation = + inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION); + return (variation + == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) + || (variation + == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)) + || (variation + == (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD)); + } + + // Please refer to TextView.isVisiblePasswordInputType + public static boolean isVisiblePasswordInputType(int inputType) { + final int variation = + inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION); + return variation + == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); + } + + public static boolean containsInCsv(String key, String csv) { + if (csv == null) + return false; + for (String option : csv.split(",")) { + if (option.equals(key)) + return true; + } + return false; + } + + public static boolean inPrivateImeOptions(String packageName, String key, + EditorInfo attribute) { + if (attribute == null) + return false; + return containsInCsv(packageName != null ? packageName + "." + key : key, + attribute.privateImeOptions); + } + + /** + * Returns a main dictionary resource id + * @return main dictionary resource id + */ + public static int getMainDictionaryResourceId(Resources res) { + return res.getIdentifier("main", "raw", LatinIME.class.getPackage().getName()); + } + + public static void loadNativeLibrary() { + try { + System.loadLibrary("jni_latinime"); + } catch (UnsatisfiedLinkError ule) { + Log.e(TAG, "Could not load native library jni_latinime"); + } + } } diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java new file mode 100644 index 000000000..2389d4e3c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java @@ -0,0 +1,99 @@ +/* + * 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.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.HashMap; + +public class WhitelistDictionary extends Dictionary { + + private static final boolean DBG = LatinImeLogger.sDBG; + private static final String TAG = WhitelistDictionary.class.getSimpleName(); + + private final HashMap<String, Pair<Integer, String>> mWhitelistWords = + new HashMap<String, Pair<Integer, String>>(); + + private static final WhitelistDictionary sInstance = new WhitelistDictionary(); + + private WhitelistDictionary() { + } + + public static WhitelistDictionary init(Context context) { + synchronized (sInstance) { + if (context != null) { + sInstance.initWordlist( + context.getResources().getStringArray(R.array.wordlist_whitelist)); + } else { + sInstance.mWhitelistWords.clear(); + } + } + return sInstance; + } + + private void initWordlist(String[] wordlist) { + mWhitelistWords.clear(); + final int N = wordlist.length; + if (N % 3 != 0) { + if (DBG) { + Log.d(TAG, "The number of the whitelist is invalid."); + } + return; + } + try { + for (int i = 0; i < N; i += 3) { + final int score = Integer.valueOf(wordlist[i]); + final String before = wordlist[i + 1]; + final String after = wordlist[i + 2]; + if (before != null && after != null) { + mWhitelistWords.put( + before.toLowerCase(), new Pair<Integer, String>(score, after)); + } + } + } catch (NumberFormatException e) { + if (DBG) { + Log.d(TAG, "The score of the word is invalid."); + } + } + } + + public String getWhiteListedWord(String before) { + if (before == null) return null; + final String lowerCaseBefore = before.toLowerCase(); + if(mWhitelistWords.containsKey(lowerCaseBefore)) { + if (DBG) { + Log.d(TAG, "--- found whiteListedWord: " + lowerCaseBefore); + } + return mWhitelistWords.get(lowerCaseBefore).second; + } + return null; + } + + // Not used for WhitelistDictionary. We use getWhitelistedWord() in Suggest.java instead + @Override + public void getWords(WordComposer composer, WordCallback callback) { + } + + @Override + public boolean isValidWord(CharSequence word) { + if (TextUtils.isEmpty(word)) return false; + return !TextUtils.isEmpty(getWhiteListedWord(word.toString())); + } +} diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 2e415b771..02583895b 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,22 +16,32 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.keyboard.KeyDetector; + import java.util.ArrayList; /** * A place to store the currently composing word with information such as adjacent key codes as well */ 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 final ArrayList<int[]> mCodes; - + + private int mTypedLength; + private final int[] mXCoordinates; + private final int[] mYCoordinates; + /** * The word chosen from the candidate list, until it is committed. */ private String mPreferredWord; - + private final StringBuilder mTypedWord; private int mCapsCount; @@ -44,17 +54,24 @@ public class WordComposer { private boolean mIsFirstCharCapitalized; public WordComposer() { - mCodes = new ArrayList<int[]>(12); - mTypedWord = new StringBuilder(20); + final int N = BinaryDictionary.MAX_WORD_LENGTH; + mCodes = new ArrayList<int[]>(N); + mTypedWord = new StringBuilder(N); + mTypedLength = 0; + mXCoordinates = new int[N]; + mYCoordinates = new int[N]; } - WordComposer(WordComposer copy) { - mCodes = new ArrayList<int[]>(copy.mCodes); - mPreferredWord = copy.mPreferredWord; - mTypedWord = new StringBuilder(copy.mTypedWord); - mCapsCount = copy.mCapsCount; - mAutoCapitalized = copy.mAutoCapitalized; - mIsFirstCharCapitalized = copy.mIsFirstCharCapitalized; + WordComposer(WordComposer source) { + mCodes = new ArrayList<int[]>(source.mCodes); + mPreferredWord = source.mPreferredWord; + mTypedWord = new StringBuilder(source.mTypedWord); + mCapsCount = source.mCapsCount; + mAutoCapitalized = source.mAutoCapitalized; + mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; + mTypedLength = source.mTypedLength; + mXCoordinates = source.mXCoordinates; + mYCoordinates = source.mYCoordinates; } /** @@ -62,6 +79,7 @@ public class WordComposer { */ public void reset() { mCodes.clear(); + mTypedLength = 0; mIsFirstCharCapitalized = false; mPreferredWord = null; mTypedWord.setLength(0); @@ -85,15 +103,28 @@ public class WordComposer { return mCodes.get(index); } + public int[] getXCoordinates() { + return mXCoordinates; + } + + public int[] getYCoordinates() { + return mYCoordinates; + } + /** * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of * the array containing unicode for adjacent keys, sorted by reducing probability/proximity. * @param codes the array of unicode values */ - public void add(int primaryCode, int[] codes) { + public void add(int primaryCode, int[] codes, int x, int y) { mTypedWord.append((char) primaryCode); correctPrimaryJuxtapos(primaryCode, codes); mCodes.add(codes); + if (mTypedLength < BinaryDictionary.MAX_WORD_LENGTH) { + mXCoordinates[mTypedLength] = x; + mYCoordinates[mTypedLength] = y; + } + ++mTypedLength; if (Character.isUpperCase((char) primaryCode)) mCapsCount++; } @@ -124,6 +155,9 @@ public class WordComposer { mTypedWord.deleteCharAt(lastPos); if (Character.isUpperCase(last)) mCapsCount--; } + if (mTypedLength > 0) { + --mTypedLength; + } } /** |