diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
33 files changed, 2555 insertions, 2096 deletions
diff --git a/java/src/com/android/inputmethod/latin/AccessibilityUtils.java b/java/src/com/android/inputmethod/latin/AccessibilityUtils.java deleted file mode 100644 index cd3f9e0ad..000000000 --- a/java/src/com/android/inputmethod/latin/AccessibilityUtils.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java new file mode 100644 index 000000000..074ecacc5 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java @@ -0,0 +1,52 @@ +/* + * 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 java.io.File; + +/** + * Immutable class to hold the address of an asset. + * As opposed to a normal file, an asset is usually represented as a contiguous byte array in + * the package file. Open it correctly thus requires the name of the package it is in, but + * also the offset in the file and the length of this data. This class encapsulates these three. + */ +class AssetFileAddress { + public final String mFilename; + public final long mOffset; + public final long mLength; + + public AssetFileAddress(final String filename, final long offset, final long length) { + mFilename = filename; + mOffset = offset; + mLength = length; + } + + public static AssetFileAddress makeFromFileName(final String filename) { + if (null == filename) return null; + File f = new File(filename); + if (null == f || !f.isFile()) return null; + return new AssetFileAddress(filename, 0l, f.length()); + } + + public static AssetFileAddress makeFromFileNameAndOffset(final String filename, + final long offset, final long length) { + if (null == filename) return null; + File f = new File(filename); + if (null == f || !f.isFile()) return null; + return new AssetFileAddress(filename, offset, length); + } +} diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java index 092f7ad63..d3119792c 100644 --- a/java/src/com/android/inputmethod/latin/AutoCorrection.java +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -48,7 +48,7 @@ public class AutoCorrection { } public void updateAutoCorrectionStatus(Map<String, Dictionary> dictionaries, - WordComposer wordComposer, ArrayList<CharSequence> suggestions, int[] priorities, + WordComposer wordComposer, ArrayList<CharSequence> suggestions, int[] sortedScores, CharSequence typedWord, double autoCorrectionThreshold, int correctionMode, CharSequence quickFixedWord, CharSequence whitelistedWord) { if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) { @@ -62,7 +62,7 @@ public class AutoCorrection { mHasAutoCorrection = true; mAutoCorrectionWord = quickFixedWord; } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions, correctionMode, - priorities, typedWord, autoCorrectionThreshold)) { + sortedScores, typedWord, autoCorrectionThreshold)) { mHasAutoCorrection = true; mAutoCorrectionWord = suggestions.get(0); } @@ -114,13 +114,13 @@ public class AutoCorrection { } private boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer, - ArrayList<CharSequence> suggestions, int correctionMode, int[] priorities, + ArrayList<CharSequence> suggestions, int correctionMode, int[] sortedScores, 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) { + && typedWord != null && suggestions.size() > 0 && sortedScores.length > 0) { final CharSequence autoCorrectionCandidate = suggestions.get(0); - final int autoCorrectionCandidateScore = priorities[0]; + final int autoCorrectionCandidateScore = sortedScores[0]; // TODO: when the normalized score of the first suggestion is nearly equals to // the normalized score of the second suggestion, behave less aggressive. mNormalizedScore = Utils.calcNormalizedScore( diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 54c6f309c..460930f16 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -27,7 +27,6 @@ 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; @@ -42,13 +41,8 @@ public class AutoDictionary extends ExpandableDictionary { static final int FREQUENCY_FOR_PICKED = 3; // Weight added to a user typing a new word that doesn't get corrected (or is reverted) static final int FREQUENCY_FOR_TYPED = 1; - // A word that is frequently typed and gets promoted to the user dictionary, uses this - // frequency. - static final int FREQUENCY_FOR_AUTO_ADD = 250; // If the user touches a typed word 2 times or more, it will become valid. private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED; - // If the user touches a typed word 4 times or more, it will be added to the user dict. - private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED; private LatinIME mIme; // Locale for which this auto dictionary is storing words @@ -152,11 +146,6 @@ public class AutoDictionary extends ExpandableDictionary { freq = freq < 0 ? addFrequency : freq + addFrequency; super.addWord(word, freq); - if (freq >= PROMOTION_THRESHOLD) { - mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD); - freq = 0; - } - synchronized (mPendingWritesLock) { // Write a null frequency if it is to be deleted from the db mPendingWrites.put(word, freq == 0 ? null : new Integer(freq)); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 08ddd25fa..9748d6006 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -21,10 +21,7 @@ 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; /** @@ -32,8 +29,11 @@ import java.util.Arrays; */ public class BinaryDictionary extends Dictionary { + public static final String DICTIONARY_PACK_AUTHORITY = + "com.android.inputmethod.latin.dictionarypack"; + /** - * There is difference between what java and native code can handle. + * There is a difference between what java and native code can handle. * This value should only be used in BinaryDictionary.java * It is necessary to keep it at this value because some languages e.g. German have * really long words. @@ -41,101 +41,59 @@ public class BinaryDictionary extends Dictionary { public static final int MAX_WORD_LENGTH = 48; public static final int MAX_WORDS = 18; + @SuppressWarnings("unused") private static final String TAG = "BinaryDictionary"; 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; - private static final BinaryDictionary sInstance = new BinaryDictionary(); private int mDicTypeId; private int mNativeDict; - private long mDictLength; 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]; + private final int[] mScores = new int[MAX_WORDS]; + private final int[] mBigramScores = new int[MAX_BIGRAMS]; 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() { - } + public static final Flag FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING = + new Flag(R.bool.config_require_umlaut_processing, 0x1); - /** - * Initialize a dictionary from a raw resource file - * @param context application context for reading resources - * @param resId the resource containing the raw binary dictionary - * @return initialized instance of BinaryDictionary - */ - public static BinaryDictionary initDictionary(Context context, int resId, int dicTypeId) { - synchronized (sInstance) { - sInstance.closeInternal(); - 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; - } + // Can create a new flag from extravalue : + // public static final Flag FLAG_MYFLAG = + // new Flag("my_flag", 0x02); - /* 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 static final Flag[] 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. + FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING, + }; - private void initFlags() { - int flags = 0; - for (Flags.FlagEntry entry : Flags.ALL_FLAGS) { - if (mSubtypeSwitcher.currentSubtypeContainsExtraValueKey(entry.mName)) - flags |= entry.mValue; - } - mFlags = flags; + private int mFlags = 0; + + /** + * Constructor for the binary dictionary. This is supposed to be called from the + * dictionary factory. + * All implementations should pass null into flagArray, except for testing purposes. + * @param context the context to access the environment from. + * @param filename the name of the file to read through native code. + * @param offset the offset of the dictionary data within the file. + * @param length the length of the binary data. + * @param flagArray the flags to limit the dictionary to, or null for default. + */ + public BinaryDictionary(final Context context, + final String filename, final long offset, final long length, Flag[] flagArray) { + // Note: at the moment a binary dictionary is always of the "main" type. + // Initializing this here will help transitioning out of the scheme where + // the Suggest class knows everything about every single dictionary. + mDicTypeId = Suggest.DIC_MAIN; + // TODO: Stop relying on the state of SubtypeSwitcher, get it as a parameter + mFlags = Flag.initFlags(null == flagArray ? ALL_FLAGS : flagArray, context, + SubtypeSwitcher.getInstance()); + loadDictionary(filename, offset, length); } static { @@ -149,16 +107,15 @@ public class BinaryDictionary extends Dictionary { private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates, int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars, - int[] frequencies); + int[] scores); private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, - int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies, + int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores, int maxWordLength, int maxBigrams, int maxAlternatives); private final void loadDictionary(String path, long startOffset, long length) { mNativeDict = openNative(path, startOffset, length, - TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER, + TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PROXIMITY_CHARS_SIZE); - mDictLength = length; } @Override @@ -168,27 +125,32 @@ public class BinaryDictionary extends Dictionary { char[] chars = previousWord.toString().toCharArray(); Arrays.fill(mOutputChars_bigrams, (char) 0); - Arrays.fill(mFrequencies_bigrams, 0); + Arrays.fill(mBigramScores, 0); int codesSize = codes.size(); + if (codesSize <= 0) { + // Do not return bigrams from BinaryDictionary when nothing was typed. + // Only use user-history bigrams (or whatever other bigram dictionaries decide). + return; + } Arrays.fill(mInputCodes, -1); int[] alternatives = codes.getCodesAt(0); System.arraycopy(alternatives, 0, mInputCodes, 0, 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, + mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS, MAX_PROXIMITY_CHARS_SIZE); for (int j = 0; j < count; ++j) { - if (mFrequencies_bigrams[j] < 1) break; + if (mBigramScores[j] < 1) break; final int start = j * MAX_WORD_LENGTH; int len = 0; while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) { ++len; } if (len > 0) { - callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j], + callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j], mDicTypeId, DataType.BIGRAM); } } @@ -197,17 +159,17 @@ public class BinaryDictionary extends Dictionary { @Override public void getWords(final WordComposer codes, final WordCallback callback) { final int count = getSuggestions(codes, mKeyboardSwitcher.getLatinKeyboard(), - mOutputChars, mFrequencies); + mOutputChars, mScores); for (int j = 0; j < count; ++j) { - if (mFrequencies[j] < 1) break; + if (mScores[j] < 1) break; final int start = j * MAX_WORD_LENGTH; int len = 0; while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { ++len; } if (len > 0) { - callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId, + callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId, DataType.UNIGRAM); } } @@ -218,7 +180,7 @@ public class BinaryDictionary extends Dictionary { } /* package for test */ int getSuggestions(final WordComposer codes, final Keyboard keyboard, - char[] outputChars, int[] frequencies) { + char[] outputChars, int[] scores) { if (!isValidDictionary()) return -1; final int codesSize = codes.size(); @@ -232,12 +194,13 @@ public class BinaryDictionary extends Dictionary { Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE)); } Arrays.fill(outputChars, (char) 0); - Arrays.fill(frequencies, 0); + Arrays.fill(scores, 0); + final int proximityInfo = keyboard == null ? 0 : keyboard.getProximityInfo(); return getSuggestionsNative( - mNativeDict, keyboard.getProximityInfo(), + mNativeDict, proximityInfo, codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize, - mFlags, outputChars, frequencies); + mFlags, outputChars, scores); } @Override @@ -247,10 +210,6 @@ public class BinaryDictionary extends Dictionary { return isValidWordNative(mNativeDict, chars, chars.length); } - public long getSize() { - return mDictLength; // This value is initialized in loadDictionary() - } - @Override public synchronized void close() { closeInternal(); @@ -260,7 +219,6 @@ public class BinaryDictionary extends Dictionary { if (mNativeDict != 0) { closeNative(mNativeDict); mNativeDict = 0; - mDictLength = 0; } } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java new file mode 100644 index 000000000..76a230f82 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -0,0 +1,151 @@ +/* + * 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.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.text.TextUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Group class for static methods to help with creation and getting of the binary dictionary + * file from the dictionary provider + */ +public class BinaryDictionaryFileDumper { + /** + * The size of the temporary buffer to copy files. + */ + static final int FILE_READ_BUFFER_SIZE = 1024; + + // Prevents this class to be accidentally instantiated. + private BinaryDictionaryFileDumper() { + } + + /** + * Generates a file name that matches the locale passed as an argument. + * The file name is basically the result of the .toString() method, except we replace + * any @File.separator with an underscore to avoid generating a file name that may not + * be created. + * @param locale the locale for which to get the file name + * @param context the context to use for getting the directory + * @return the name of the file to be created + */ + private static String getCacheFileNameForLocale(Locale locale, Context context) { + // The following assumes two things : + // 1. That File.separator is not the same character as "_" + // I don't think any android system will ever use "_" as a path separator + // 2. That no two locales differ by only a File.separator versus a "_" + // Since "_" can't be part of locale components this should be safe. + // Examples: + // en -> en + // en_US_POSIX -> en_US_POSIX + // en__foo/bar -> en__foo_bar + final String[] separator = { File.separator }; + final String[] empty = { "_" }; + final CharSequence basename = TextUtils.replace(locale.toString(), separator, empty); + return context.getFilesDir() + File.separator + basename; + } + + /** + * Return for a given locale the provider URI to query to get the dictionary. + */ + public static Uri getProviderUri(Locale locale) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath( + locale.toString()).build(); + } + + /** + * Queries a content provider for dictionary data for some locale and returns the file addresses + * + * This will query a content provider for dictionary data for a given locale, and return + * the addresses of a file set the members of which are suitable to be mmap'ed. It will copy + * them to local storage if needed. + * It should also check the dictionary versions to avoid unnecessary copies but this is + * still in TODO state. + * This will make the data from the content provider the cached dictionary for this locale, + * overwriting any previous cached data. + * @returns the addresses of the files, or null if no data could be obtained. + * @throw FileNotFoundException if the provider returns non-existent data. + * @throw IOException if the provider-returned data could not be read. + */ + public static List<AssetFileAddress> getDictSetFromContentProvider(Locale locale, + Context context) throws FileNotFoundException, IOException { + // TODO: check whether the dictionary is the same or not and if it is, return the cached + // file. + // TODO: This should be able to read a number of files from the dictionary pack, copy + // them all and return them. + final ContentResolver resolver = context.getContentResolver(); + final Uri dictionaryPackUri = getProviderUri(locale); + final AssetFileDescriptor afd = resolver.openAssetFileDescriptor(dictionaryPackUri, "r"); + if (null == afd) return null; + final String fileName = + copyFileTo(afd.createInputStream(), getCacheFileNameForLocale(locale, context)); + return Arrays.asList(AssetFileAddress.makeFromFileName(fileName)); + } + + /** + * Accepts a file as dictionary data for some locale and returns the name of a file. + * + * This will make the data in the input file the cached dictionary for this locale, overwriting + * any previous cached data. + */ + public static String getDictionaryFileFromFile(String fileName, Locale locale, + Context context) throws FileNotFoundException, IOException { + return copyFileTo(new FileInputStream(fileName), getCacheFileNameForLocale(locale, + context)); + } + + /** + * Accepts a resource number as dictionary data for some locale and returns the name of a file. + * + * This will make the resource the cached dictionary for this locale, overwriting any previous + * cached data. + */ + public static String getDictionaryFileFromResource(int resource, Locale locale, + Context context) throws FileNotFoundException, IOException { + return copyFileTo(context.getResources().openRawResource(resource), + getCacheFileNameForLocale(locale, context)); + } + + /** + * Copies the data in an input stream to a target file, creating the file if necessary and + * overwriting it if it already exists. + * @param input the stream to be copied. + * @param outputFileName the name of a file to copy the data to. It is created if necessary. + */ + private static String copyFileTo(final InputStream input, final String outputFileName) + throws FileNotFoundException, IOException { + final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE]; + final FileOutputStream output = new FileOutputStream(outputFileName); + for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) + output.write(buffer, 0, readBytes); + input.close(); + return outputFileName; + } +} diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java new file mode 100644 index 000000000..7ce92920d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -0,0 +1,97 @@ +/* + * 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.res.AssetFileDescriptor; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Helper class to get the address of a mmap'able dictionary file. + */ +class BinaryDictionaryGetter { + + /** + * Used for Log actions from this class + */ + private static final String TAG = BinaryDictionaryGetter.class.getSimpleName(); + + // Prevents this from being instantiated + private BinaryDictionaryGetter() {} + + /** + * Returns a file address from a resource, or null if it cannot be opened. + */ + private static AssetFileAddress loadFallbackResource(Context context, int fallbackResId) { + final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId); + if (afd == null) { + Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId=" + + fallbackResId); + return null; + } + return AssetFileAddress.makeFromFileNameAndOffset( + context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength()); + } + + /** + * Returns a list of file addresses for a given locale, trying relevant methods in order. + * + * Tries to get binary dictionaries from various sources, in order: + * - Uses a private method of getting a private dictionaries, as implemented by the + * PrivateBinaryDictionaryGetter class. + * If that fails: + * - Uses a content provider to get a public dictionary set, as per the protocol described + * in BinaryDictionaryFileDumper. + * If that fails: + * - Gets a file name from the fallback resource passed as an argument. + * If that fails: + * - Returns null. + * @return The address of a valid file, or null. + */ + public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context, + int fallbackResId) { + // Try first to query a private package signed the same way for private files. + final List<AssetFileAddress> privateFiles = + PrivateBinaryDictionaryGetter.getDictionaryFiles(locale, context); + if (null != privateFiles) { + return privateFiles; + } else { + try { + // If that was no-go, try to find a publicly exported dictionary. + List<AssetFileAddress> listFromContentProvider = + BinaryDictionaryFileDumper.getDictSetFromContentProvider(locale, context); + if (null != listFromContentProvider) { + return listFromContentProvider; + } + // If the list is null, fall through and return the fallback + } catch (FileNotFoundException e) { + Log.e(TAG, "Unable to create dictionary file from provider for locale " + + locale.toString() + ": falling back to internal dictionary"); + } catch (IOException e) { + Log.e(TAG, "Unable to read source data for locale " + + locale.toString() + ": falling back to internal dictionary"); + } + return Arrays.asList(loadFallbackResource(context, fallbackResId)); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index c52f6b2c4..a8381020f 100644 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; - import android.content.Context; import android.content.res.Resources; import android.graphics.Color; @@ -40,33 +38,41 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.TextView; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + import java.util.ArrayList; import java.util.List; public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener { + public interface Listener { + public boolean addWordToDictionary(String word); + public void pickSuggestionManually(int index, CharSequence word); + } + private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); private static final int MAX_SUGGESTIONS = 16; private static final boolean DBG = LatinImeLogger.sDBG; - private final ArrayList<View> mWords = new ArrayList<View>(); + private final ArrayList<TextView> mWords = new ArrayList<TextView>(); + private final ArrayList<View> mDividers = new ArrayList<View>(); + private final int mCandidatePadding; private final boolean mConfigCandidateHighlightFontColorEnabled; private final CharacterStyle mInvertedForegroundColorSpan; private final CharacterStyle mInvertedBackgroundColorSpan; - private final int mColorNormal; - private final int mColorRecommended; - private final int mColorOther; + private final int mColorTypedWord; + private final int mColorAutoCorrect; + private final int mColorSuggestedCandidate; private final PopupWindow mPreviewPopup; private final TextView mPreviewText; - private LatinIME mService; + private Listener mListener; private SuggestedWords mSuggestions = SuggestedWords.EMPTY; private boolean mShowingAutoCorrectionInverted; private boolean mShowingAddToDictionary; @@ -133,37 +139,37 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo ViewGroup.LayoutParams.WRAP_CONTENT); mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); - mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation); mConfigCandidateHighlightFontColorEnabled = res.getBoolean(R.bool.config_candidate_highlight_font_color_enabled); - mColorNormal = res.getColor(R.color.candidate_normal); - mColorRecommended = res.getColor(R.color.candidate_recommended); - mColorOther = res.getColor(R.color.candidate_other); - mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorNormal ^ 0x00ffffff); - mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorNormal); + mColorTypedWord = res.getColor(R.color.candidate_typed_word); + mColorAutoCorrect = res.getColor(R.color.candidate_auto_correct); + mColorSuggestedCandidate = res.getColor(R.color.candidate_suggested); + mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff); + mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord); + mCandidatePadding = res.getDimensionPixelOffset(R.dimen.candidate_padding); for (int i = 0; i < MAX_SUGGESTIONS; i++) { - View v = inflater.inflate(R.layout.candidate, null); - TextView tv = (TextView)v.findViewById(R.id.candidate_word); + final TextView tv = (TextView)inflater.inflate(R.layout.candidate, null); tv.setTag(i); tv.setOnClickListener(this); if (i == 0) tv.setOnLongClickListener(this); - ImageView divider = (ImageView)v.findViewById(R.id.candidate_divider); - // Do not display divider of first candidate. - divider.setVisibility(i == 0 ? GONE : VISIBLE); - mWords.add(v); + mWords.add(tv); + if (i > 0) { + View divider = inflater.inflate(R.layout.candidate_divider, null); + mDividers.add(divider); + } } scrollTo(0, getScrollY()); } /** - * A connection back to the service to communicate with the text field + * A connection back to the input method. * @param listener */ - public void setService(LatinIME listener) { - mService = listener; + public void setListener(Listener listener) { + mListener = listener; } public void setSuggestions(SuggestedWords suggestions) { @@ -177,68 +183,79 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } } + private CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) { + if (!isAutoCorrect) + return word; + final CharacterStyle style = mConfigCandidateHighlightFontColorEnabled ? BOLD_SPAN + : UNDERLINE_SPAN; + final Spannable spannedWord = new SpannableString(word); + spannedWord.setSpan(style, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + return spannedWord; + } + + private int getCandidateTextColor(boolean isAutoCorrect, boolean isSuggestedCandidate, + SuggestedWordInfo info) { + final int color; + if (isAutoCorrect && mConfigCandidateHighlightFontColorEnabled) { + color = mColorAutoCorrect; + } else if (isSuggestedCandidate) { + color = mColorSuggestedCandidate; + } else { + color = mColorTypedWord; + } + if (info != null && info.isPreviousSuggestedWord()) { + final int newAlpha = (int)(Color.alpha(color) * 0.5f); + return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); + } else { + return color; + } + } + private void updateSuggestions() { final SuggestedWords suggestions = mSuggestions; + final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList; + clear(); - final int count = suggestions.size(); + final int count = Math.min(mWords.size(), suggestions.size()); for (int i = 0; i < count; i++) { - CharSequence word = suggestions.getWord(i); + final CharSequence word = suggestions.getWord(i); if (word == null) continue; - final int wordLength = word.length(); - final List<SuggestedWordInfo> suggestedWordInfoList = - suggestions.mSuggestedWordInfoList; - - final View v = mWords.get(i); - final TextView tv = (TextView)v.findViewById(R.id.candidate_word); - final TextView dv = (TextView)v.findViewById(R.id.candidate_debug_info); - tv.setTextColor(mColorNormal); - // TODO: Needs safety net? - if (suggestions.mHasMinimalSuggestion + + final SuggestedWordInfo info = (suggestedWordInfoList != null) + ? suggestedWordInfoList.get(i) : null; + final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion && ((i == 1 && !suggestions.mTypedWordValid) - || (i == 0 && suggestions.mTypedWordValid))) { - final CharacterStyle style; - if (mConfigCandidateHighlightFontColorEnabled) { - style = BOLD_SPAN; - tv.setTextColor(mColorRecommended); - } else { - style = UNDERLINE_SPAN; - } - final Spannable spannedWord = new SpannableString(word); - spannedWord.setSpan(style, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - word = spannedWord; - } else if (i != 0 || (wordLength == 1 && count > 1)) { - // HACK: even if i == 0, we use mColorOther when this - // suggestion's length is 1 - // and there are multiple suggestions, such as the default - // punctuation list. - if (mConfigCandidateHighlightFontColorEnabled) - tv.setTextColor(mColorOther); - } - tv.setText(word); - tv.setClickable(true); - - if (suggestedWordInfoList != null && suggestedWordInfoList.get(i) != null) { - final SuggestedWordInfo info = suggestedWordInfoList.get(i); - if (info.isPreviousSuggestedWord()) { - int color = tv.getCurrentTextColor(); - tv.setTextColor(Color.argb((int)(Color.alpha(color) * 0.5f), Color.red(color), - Color.green(color), Color.blue(color))); - } - final String debugString = info.getDebugString(); - if (DBG) { - if (TextUtils.isEmpty(debugString)) { - dv.setVisibility(GONE); - } else { - dv.setText(debugString); - dv.setVisibility(VISIBLE); - } - } else { - dv.setVisibility(GONE); - } + || (i == 0 && suggestions.mTypedWordValid)); + // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 + // and there are multiple suggestions, such as the default punctuation list. + // TODO: Need to revisit this logic with bigram suggestions + final boolean isSuggestedCandidate = (i != 0); + final boolean isPunctuationSuggestions = (word.length() == 1 && count > 1); + + final TextView tv = mWords.get(i); + tv.setTextColor(getCandidateTextColor(isAutoCorrect, + isSuggestedCandidate || isPunctuationSuggestions, info)); + tv.setText(getStyledCandidateWord(word, isAutoCorrect)); + if (i == 0) { + tv.setPadding(mCandidatePadding, 0, 0, 0); + } else if (i == count - 1) { + tv.setPadding(0, 0, mCandidatePadding, 0); } else { - dv.setVisibility(GONE); + tv.setPadding(0, 0, 0, 0); + } + if (i > 0) + addView(mDividers.get(i - 1)); + addView(tv); + + if (DBG && info != null) { + final TextView dv = new TextView(getContext(), null); + dv.setTextSize(10.0f); + dv.setTextColor(0xff808080); + dv.setText(info.getDebugString()); + addView(dv); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)dv.getLayoutParams(); + lp.gravity = Gravity.BOTTOM; } - addView(v); } scrollTo(0, getScrollY()); @@ -250,7 +267,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo // with color is disabled. if (mConfigCandidateHighlightFontColorEnabled) return; - final TextView tv = (TextView)mWords.get(1).findViewById(R.id.candidate_word); + final TextView tv = mWords.get(1); final Spannable word = new SpannableString(autoCorrectedWord); final int wordLength = word.length(); word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength, @@ -276,7 +293,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo setSuggestions(builder.build()); mShowingAddToDictionary = true; // Disable R.string.hint_add_to_dictionary button - TextView tv = (TextView)getChildAt(1).findViewById(R.id.candidate_word); + TextView tv = mWords.get(1); tv.setClickable(false); } @@ -305,11 +322,11 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo return; final TextView previewText = mPreviewText; - previewText.setTextColor(mColorNormal); + previewText.setTextColor(mColorTypedWord); previewText.setText(word); previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - View v = getChildAt(index); + View v = mWords.get(index); final int[] offsetInWindow = new int[2]; v.getLocationInWindow(offsetInWindow); final int posX = offsetInWindow[0]; @@ -325,7 +342,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } private void addToDictionary(CharSequence word) { - if (mService.addWordToDictionary(word.toString())) { + if (mListener.addWordToDictionary(word.toString())) { showPreview(0, getContext().getString(R.string.added_word, word)); } } @@ -351,7 +368,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo if (mShowingAddToDictionary && index == 0) { addToDictionary(word); } else { - mService.pickSuggestionManually(index, word); + mListener.pickSuggestionManually(index, word); } } diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java index 048f72dc5..66a041508 100644 --- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -26,6 +26,8 @@ import android.provider.ContactsContract.Contacts; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.keyboard.Keyboard; + public class ContactsDictionary extends ExpandableDictionary { private static final String[] PROJECTION = { @@ -38,7 +40,7 @@ public class ContactsDictionary extends ExpandableDictionary { /** * Frequency for contacts information into the dictionary */ - private static final int FREQUENCY_FOR_CONTACTS = 128; + private static final int FREQUENCY_FOR_CONTACTS = 40; private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90; private static final int INDEX_NAME = 1; @@ -95,6 +97,14 @@ public class ContactsDictionary extends ExpandableDictionary { mLastLoadedContacts = SystemClock.uptimeMillis(); } + @Override + public void getBigrams(final WordComposer codes, final CharSequence previousWord, + final WordCallback callback) { + // Do not return bigrams from Contacts when nothing was typed. + if (codes.size() <= 0) return; + super.getBigrams(codes, previousWord, callback); + } + private void addWords(Cursor cursor) { clearDictionary(); @@ -104,7 +114,7 @@ public class ContactsDictionary extends ExpandableDictionary { while (!cursor.isAfterLast()) { String name = cursor.getString(INDEX_NAME); - if (name != null) { + if (name != null && -1 == name.indexOf('@')) { int len = name.length(); String prevWord = null; @@ -115,8 +125,9 @@ public class ContactsDictionary extends ExpandableDictionary { for (j = i + 1; j < len; j++) { char c = name.charAt(j); - if (!(c == '-' || c == '\'' || - Character.isLetter(c))) { + if (!(c == Keyboard.CODE_DASH + || c == Keyboard.CODE_SINGLE_QUOTE + || Character.isLetter(c))) { break; } } @@ -132,8 +143,6 @@ public class ContactsDictionary extends ExpandableDictionary { if (wordLen < maxWordLength && wordLen > 1) { super.addWord(word, FREQUENCY_FOR_CONTACTS); if (!TextUtils.isEmpty(prevWord)) { - // TODO Do not add email address - // Not so critical super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM); } diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java index 2f1e7c2b8..fd62d61c3 100644 --- a/java/src/com/android/inputmethod/latin/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/DebugSettings.java @@ -33,6 +33,7 @@ public class DebugSettings extends PreferenceActivity private boolean mServiceNeedsRestart = false; private CheckBoxPreference mDebugMode; + private CheckBoxPreference mUseSpacebarLanguageSwitch; @Override protected void onCreate(Bundle icicle) { @@ -60,6 +61,13 @@ public class DebugSettings extends PreferenceActivity updateDebugMode(); mServiceNeedsRestart = true; } + } else if (key.equals(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY)) { + if (mUseSpacebarLanguageSwitch != null) { + mUseSpacebarLanguageSwitch.setChecked( + prefs.getBoolean(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY, + getResources().getBoolean( + R.bool.config_use_spacebar_language_switcher))); + } } } diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 56f0cc503..c7737b9a2 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -29,7 +29,7 @@ public abstract class Dictionary { /** * The weight to give to a word if it's length is the same as the number of typed characters. */ - protected static final int FULL_WORD_FREQ_MULTIPLIER = 2; + protected static final int FULL_WORD_SCORE_MULTIPLIER = 2; public static enum DataType { UNIGRAM, BIGRAM @@ -42,17 +42,17 @@ public abstract class Dictionary { public interface WordCallback { /** * Adds a word to a list of suggestions. The word is expected to be ordered based on - * the provided frequency. + * the provided score. * @param word the character array containing the word * @param wordOffset starting offset of the word in the character array * @param wordLength length of valid characters in the character array - * @param frequency the frequency of occurrence. This is normalized between 1 and 255, but + * @param score the score of occurrence. This is normalized between 1 and 255, but * can exceed those limits * @param dicTypeId of the dictionary where word was from * @param dataType tells type of this data * @return true if the word was added, false if no more words are required */ - boolean addWord(char[] word, int wordOffset, int wordLength, int frequency, int dicTypeId, + boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId, DataType dataType); } @@ -61,7 +61,7 @@ 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 - * @see WordCallback#addWord(char[], int, int) + * @see WordCallback#addWord(char[], int, int, int, int, DataType) */ abstract public void getWords(final WordComposer composer, final WordCallback callback); diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java new file mode 100644 index 000000000..3fcb6ed55 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -0,0 +1,71 @@ +/* + * 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 java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Class for a collection of dictionaries that behave like one dictionary. + */ +public class DictionaryCollection extends Dictionary { + + protected final List<Dictionary> mDictionaries; + + public DictionaryCollection() { + mDictionaries = new CopyOnWriteArrayList<Dictionary>(); + } + + public DictionaryCollection(Dictionary... dictionaries) { + mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries); + } + + public DictionaryCollection(Collection<Dictionary> dictionaries) { + mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries); + } + + @Override + public void getWords(final WordComposer composer, final WordCallback callback) { + for (final Dictionary dict : mDictionaries) + dict.getWords(composer, callback); + } + + @Override + public void getBigrams(final WordComposer composer, final CharSequence previousWord, + final WordCallback callback) { + for (final Dictionary dict : mDictionaries) + dict.getBigrams(composer, previousWord, callback); + } + + @Override + public boolean isValidWord(CharSequence word) { + for (final Dictionary dict : mDictionaries) + if (dict.isValidWord(word)) return true; + return false; + } + + @Override + public void close() { + for (final Dictionary dict : mDictionaries) + dict.close(); + } + + public void addDictionary(Dictionary newDict) { + mDictionaries.add(newDict); + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java new file mode 100644 index 000000000..bba331868 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -0,0 +1,177 @@ +/* + * 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.res.AssetFileDescriptor; +import android.content.res.Resources; +import android.util.Log; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +/** + * Factory for dictionary instances. + */ +public class DictionaryFactory { + + private static String TAG = DictionaryFactory.class.getSimpleName(); + + /** + * Initializes a dictionary from a dictionary pack. + * + * This searches for a content provider providing a dictionary pack for the specified + * locale. If none is found, it falls back to using the resource passed as fallBackResId + * as a dictionary. + * @param context application context for reading resources + * @param locale the locale for which to create the dictionary + * @param fallbackResId the id of the resource to use as a fallback if no pack is found + * @return an initialized instance of Dictionary + */ + public static Dictionary createDictionaryFromManager(Context context, Locale locale, + int fallbackResId) { + if (null == locale) { + Log.e(TAG, "No locale defined for dictionary"); + return new DictionaryCollection(createBinaryDictionary(context, fallbackResId)); + } + + final List<Dictionary> dictList = new LinkedList<Dictionary>(); + for (final AssetFileAddress f : BinaryDictionaryGetter.getDictionaryFiles(locale, + context, fallbackResId)) { + dictList.add(new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength, null)); + } + + if (null == dictList) return null; + return new DictionaryCollection(dictList); + } + + /** + * Initializes a dictionary from a raw resource file + * @param context application context for reading resources + * @param resId the resource containing the raw binary dictionary + * @return an initialized instance of BinaryDictionary + */ + protected static BinaryDictionary createBinaryDictionary(Context context, int resId) { + AssetFileDescriptor afd = null; + try { + afd = context.getResources().openRawResourceFd(resId); + if (afd == null) { + Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); + return null; + } + if (!isFullDictionary(afd)) 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; + } + return new BinaryDictionary(context, + sourceDir, afd.getStartOffset(), afd.getLength(), null); + } catch (android.content.res.Resources.NotFoundException e) { + Log.e(TAG, "Could not find the resource. resId=" + resId); + return null; + } finally { + if (null != afd) { + try { + afd.close(); + } catch (java.io.IOException e) { + /* IOException on close ? What am I supposed to do ? */ + } + } + } + } + + /** + * Create a dictionary from passed data. This is intended for unit tests only. + * @param context the test context to create this data from. + * @param dictionary the file to read + * @param startOffset the offset in the file where the data starts + * @param length the length of the data + * @param flagArray the flags to use with this data for testing + * @return the created dictionary, or null. + */ + public static Dictionary createDictionaryForTest(Context context, File dictionary, + long startOffset, long length, Flag[] flagArray) { + if (dictionary.isFile()) { + return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length, + flagArray); + } else { + Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath()); + return null; + } + } + + /** + * Find out whether a dictionary is available for this locale. + * @param context the context on which to check resources. + * @param locale the locale to check for. + * @return whether a (non-placeholder) dictionary is available or not. + */ + public static boolean isDictionaryAvailable(Context context, Locale locale) { + final Resources res = context.getResources(); + final Locale saveLocale = Utils.setSystemLocale(res, locale); + + final int resourceId = Utils.getMainDictionaryResourceId(res); + final AssetFileDescriptor afd = res.openRawResourceFd(resourceId); + final boolean hasDictionary = isFullDictionary(afd); + try { + if (null != afd) afd.close(); + } catch (java.io.IOException e) { + /* Um, what can we do here exactly? */ + } + + Utils.setSystemLocale(res, saveLocale); + return hasDictionary; + } + + // TODO: Do not use the size of the dictionary as an unique dictionary ID. + public static Long getDictionaryId(Context context, Locale locale) { + final Resources res = context.getResources(); + final Locale saveLocale = Utils.setSystemLocale(res, locale); + + final int resourceId = Utils.getMainDictionaryResourceId(res); + final AssetFileDescriptor afd = res.openRawResourceFd(resourceId); + final Long size = (afd != null && afd.getLength() > PLACEHOLDER_LENGTH) + ? afd.getLength() + : null; + try { + if (null != afd) afd.close(); + } catch (java.io.IOException e) { + } + + Utils.setSystemLocale(res, saveLocale); + return size; + } + + // TODO: Find the Right Way to find out whether the resource is a placeholder or not. + // Suggestion : strip the locale, open the placeholder file and store its offset. + // Upon opening the file, if it's the same offset, then it's the placeholder. + private static final long PLACEHOLDER_LENGTH = 34; + /** + * Finds out whether the data pointed out by an AssetFileDescriptor is a full + * dictionary (as opposed to null, or to a place holder). + * @param afd the file descriptor to test, or null + * @return true if the dictionary is a real full dictionary, false if it's null or a placeholder + */ + protected static boolean isFullDictionary(final AssetFileDescriptor afd) { + return (afd != null && afd.getLength() > PLACEHOLDER_LENGTH); + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java new file mode 100644 index 000000000..9d30af84b --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java @@ -0,0 +1,89 @@ +/* + * 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.net.Uri; + +/** + * Takes action to reload the necessary data when a dictionary pack was added/removed. + */ +public class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver { + + final LatinIME mService; + /** + * The action of the intent for publishing that new dictionary data is available. + */ + /* package */ static final String NEW_DICTIONARY_INTENT_ACTION = + "com.android.inputmethod.latin.dictionarypack.newdict"; + + public DictionaryPackInstallBroadcastReceiver(final LatinIME service) { + mService = service; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final PackageManager manager = context.getPackageManager(); + + // We need to reread the dictionary if a new dictionary package is installed. + if (action.equals(Intent.ACTION_PACKAGE_ADDED)) { + final Uri packageUri = intent.getData(); + if (null == packageUri) return; // No package name : we can't do anything + final String packageName = packageUri.getSchemeSpecificPart(); + if (null == packageName) return; + final PackageInfo packageInfo; + try { + packageInfo = manager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS); + } catch (android.content.pm.PackageManager.NameNotFoundException e) { + return; // No package info : we can't do anything + } + final ProviderInfo[] providers = packageInfo.providers; + if (null == providers) return; // No providers : it is not a dictionary. + + // Search for some dictionary pack in the just-installed package. If found, reread. + for (ProviderInfo info : providers) { + if (BinaryDictionary.DICTIONARY_PACK_AUTHORITY.equals(info.authority)) { + mService.resetSuggestMainDict(); + return; + } + } + // If we come here none of the authorities matched the one we searched for. + // We can exit safely. + return; + } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED) + && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + // When the dictionary package is removed, we need to reread dictionary (to use the + // next-priority one, or stop using a dictionary at all if this was the only one, + // since this is the user request). + // If we are replacing the package, we will receive ADDED right away so no need to + // remove the dictionary at the moment, since we will do it when we receive the + // ADDED broadcast. + + // TODO: Only reload dictionary on REMOVED when the removed package is the one we + // read dictionary from? + mService.resetSuggestMainDict(); + } else if (action.equals(NEW_DICTIONARY_INTENT_ACTION)) { + mService.resetSuggestMainDict(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java index 90c250dcb..e56aa695d 100644 --- a/java/src/com/android/inputmethod/latin/EditingUtils.java +++ b/java/src/com/android/inputmethod/latin/EditingUtils.java @@ -16,13 +16,13 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.compat.InputConnectionCompatUtils; + import android.text.TextUtils; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.regex.Pattern; /** @@ -34,11 +34,6 @@ public class EditingUtils { */ private static final int LOOKBACK_CHARACTER_NUM = 15; - // Cache Method pointers - private static boolean sMethodsInitialized; - private static Method sMethodGetSelectedText; - private static Method sMethodSetComposingRegion; - private EditingUtils() { // Unintentional empty constructor for singleton. } @@ -78,7 +73,7 @@ public class EditingUtils { /** * @param connection connection to the current text field. - * @param sep characters which may separate words + * @param separators characters which may separate words * @return the word that surrounds the cursor, including up to one trailing * separator. For example, if the field contains "he|llo world", where | * represents the cursor, then "hello " will be returned. @@ -166,23 +161,62 @@ public class EditingUtils { private static final Pattern spaceRegex = Pattern.compile("\\s+"); + public static CharSequence getPreviousWord(InputConnection connection, String sentenceSeperators) { //TODO: Should fix this. This could be slow! CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); - if (prev == null) { - return null; - } + return getPreviousWord(prev, sentenceSeperators); + } + + // Get the word before the whitespace preceding the non-whitespace preceding the cursor. + // Also, it won't return words that end in a separator. + // Example : + // "abc def|" -> abc + // "abc def |" -> abc + // "abc def. |" -> abc + // "abc def . |" -> def + // "abc|" -> null + // "abc |" -> null + // "abc. def|" -> null + public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) { + if (prev == null) return null; String[] w = spaceRegex.split(prev); - if (w.length >= 2 && w[w.length-2].length() > 0) { - char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1); - if (sentenceSeperators.contains(String.valueOf(lastChar))) { - return null; - } - return w[w.length-2]; - } else { - return null; - } + + // If we can't find two words, or we found an empty word, return null. + if (w.length < 2 || w[w.length - 2].length() <= 0) return null; + + // If ends in a separator, return null + char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1); + if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; + + return w[w.length - 2]; + } + + public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) { + final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + return getThisWord(prev, sentenceSeperators); + } + + // Get the word immediately before the cursor, even if there is whitespace between it and + // the cursor - but not if there is punctuation. + // Example : + // "abc def|" -> def + // "abc def |" -> def + // "abc def. |" -> null + // "abc def . |" -> null + public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) { + if (prev == null) return null; + String[] w = spaceRegex.split(prev); + + // No word : return null + if (w.length < 1 || w[w.length - 1].length() <= 0) return null; + + // If ends in a separator, return null + char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1); + if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; + + return w[w.length - 1]; } public static class SelectedWord { @@ -241,7 +275,8 @@ public class EditingUtils { } // Extract the selection alone - CharSequence touching = getSelectedText(ic, selStart, selEnd); + CharSequence touching = InputConnectionCompatUtils.getSelectedText( + ic, selStart, selEnd); if (TextUtils.isEmpty(touching)) return null; // Is any part of the selection a separator? If so, return null. final int length = touching.length(); @@ -255,74 +290,4 @@ public class EditingUtils { } return null; } - - /** - * Cache method pointers for performance - */ - private static void initializeMethodsForReflection() { - try { - // These will either both exist or not, so no need for separate try/catch blocks. - // If other methods are added later, use separate try/catch blocks. - sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class); - sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion", - int.class, int.class); - } catch (NoSuchMethodException exc) { - // Ignore - } - sMethodsInitialized = true; - } - - /** - * Returns the selected text between the selStart and selEnd positions. - */ - private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) { - // Use reflection, for backward compatibility - CharSequence result = null; - if (!sMethodsInitialized) { - initializeMethodsForReflection(); - } - if (sMethodGetSelectedText != null) { - try { - result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0); - return result; - } catch (InvocationTargetException exc) { - // Ignore - } catch (IllegalArgumentException e) { - // Ignore - } catch (IllegalAccessException e) { - // Ignore - } - } - // Reflection didn't work, try it the poor way, by moving the cursor to the start, - // getting the text after the cursor and moving the text back to selected mode. - // TODO: Verify that this works properly in conjunction with - // LatinIME#onUpdateSelection - ic.setSelection(selStart, selEnd); - result = ic.getTextAfterCursor(selEnd - selStart, 0); - ic.setSelection(selStart, selEnd); - return result; - } - - /** - * Tries to set the text into composition mode if there is support for it in the framework. - */ - public static void underlineWord(InputConnection ic, SelectedWord word) { - // Use reflection, for backward compatibility - // If method not found, there's nothing we can do. It still works but just wont underline - // the word. - if (!sMethodsInitialized) { - initializeMethodsForReflection(); - } - if (sMethodSetComposingRegion != null) { - try { - sMethodSetComposingRegion.invoke(ic, word.mStart, word.mEnd); - } catch (InvocationTargetException exc) { - // Ignore - } catch (IllegalArgumentException e) { - // Ignore - } catch (IllegalAccessException e) { - // Ignore - } - } - } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 0318175f6..97a4a1816 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -19,6 +19,8 @@ package com.android.inputmethod.latin; import android.content.Context; import android.os.AsyncTask; +import com.android.inputmethod.keyboard.Keyboard; + import java.util.LinkedList; /** @@ -32,14 +34,14 @@ public class ExpandableDictionary extends Dictionary { */ protected static final int MAX_WORD_LENGTH = 32; + // Bigram frequency is a fixed point number with 1 meaning 1.2 and 255 meaning 1.8. + protected static final int BIGRAM_MAX_FREQUENCY = 255; + private Context mContext; private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; private int mDicTypeId; private int mMaxDepth; private int mInputLength; - private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH); - - private static final char QUOTE = '\''; private boolean mRequiresReload; @@ -98,6 +100,7 @@ public class ExpandableDictionary extends Dictionary { public int addFrequency(int add) { mFrequency += add; + if (mFrequency > BIGRAM_MAX_FREQUENCY) mFrequency = BIGRAM_MAX_FREQUENCY; return mFrequency; } } @@ -226,6 +229,7 @@ public class ExpandableDictionary extends Dictionary { * Returns the word's frequency or -1 if not found */ protected int getWordFrequency(CharSequence word) { + // Case-sensitive search Node node = searchNode(mRoots, word, 0, word.length()); return (node == null) ? -1 : node.mFrequency; } @@ -301,7 +305,8 @@ public class ExpandableDictionary extends Dictionary { getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, skipPos, callback); } - } else if ((c == QUOTE && currentChars[0] != QUOTE) || depth == skipPos) { + } else if ((c == Keyboard.CODE_SINGLE_QUOTE + && currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) { // Skip the ' and continue deeper word[depth] = c; if (children != null) { @@ -327,7 +332,7 @@ public class ExpandableDictionary extends Dictionary { final int finalFreq; if (skipPos < 0) { finalFreq = freq * snr * addedAttenuation - * FULL_WORD_FREQ_MULTIPLIER; + * FULL_WORD_SCORE_MULTIPLIER; } else { finalFreq = computeSkippedWordFinalFreq(freq, snr * addedAttenuation, mInputLength); @@ -362,12 +367,16 @@ public class ExpandableDictionary extends Dictionary { /** * Adds bigrams to the in-memory trie structure that is being used to retrieve any word - * @param frequency frequency for this bigrams - * @param addFrequency if true, it adds to current frequency + * @param frequency frequency for this bigram + * @param addFrequency if true, it adds to current frequency, else it overwrites the old value * @return returns the final frequency */ private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) { - Node firstWord = searchWord(mRoots, word1, 0, null); + // We don't want results to be different according to case of the looked up left hand side + // word. We do want however to return the correct case for the right hand side. + // So we want to squash the case of the left hand side, and preserve that of the right + // hand side word. + Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null); Node secondWord = searchWord(mRoots, word2, 0, null); LinkedList<NextWord> bigram = firstWord.mNGrams; if (bigram == null || bigram.size() == 0) { @@ -433,8 +442,12 @@ public class ExpandableDictionary extends Dictionary { } } - private void runReverseLookUp(final CharSequence previousWord, final WordCallback callback) { - Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length()); + private void runBigramReverseLookUp(final CharSequence previousWord, + final WordCallback callback) { + // Search for the lowercase version of the word only, because that's where bigrams + // store their sons. + Node prevWord = searchNode(mRoots, previousWord.toString().toLowerCase(), 0, + previousWord.length()); if (prevWord != null && prevWord.mNGrams != null) { reverseLookUp(prevWord.mNGrams, callback); } @@ -444,7 +457,7 @@ public class ExpandableDictionary extends Dictionary { public void getBigrams(final WordComposer codes, final CharSequence previousWord, final WordCallback callback) { if (!reloadDictionaryIfRequired()) { - runReverseLookUp(previousWord, callback); + runBigramReverseLookUp(previousWord, callback); } } @@ -462,6 +475,9 @@ public class ExpandableDictionary extends Dictionary { } } + // Local to reverseLookUp, but do not allocate each time. + private final char[] mLookedUpString = new char[MAX_WORD_LENGTH]; + /** * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words * through callback. @@ -474,30 +490,33 @@ public class ExpandableDictionary extends Dictionary { for (NextWord nextWord : terminalNodes) { node = nextWord.mWord; freq = nextWord.getFrequency(); - // TODO Not the best way to limit suggestion threshold - if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) { - sb.setLength(0); - do { - sb.insert(0, node.mCode); - node = node.mParent; - } while(node != null); - - // TODO better way to feed char array? - callback.addWord(sb.toString().toCharArray(), 0, sb.length(), freq, mDicTypeId, - DataType.BIGRAM); - } + int index = MAX_WORD_LENGTH; + do { + --index; + mLookedUpString[index] = node.mCode; + node = node.mParent; + } while (node != null); + + callback.addWord(mLookedUpString, index, MAX_WORD_LENGTH - index, freq, mDicTypeId, + DataType.BIGRAM); } } /** - * Search for the terminal node of the word + * Recursively search for the terminal node of the word. + * + * One iteration takes the full word to search for and the current index of the recursion. + * + * @param children the node of the trie to search under. + * @param word the word to search for. Only read [offset..length] so there may be trailing chars + * @param offset the index in {@code word} this recursion should operate on. + * @param length the length of the input word. * @return Returns the terminal node of the word if the word exists */ private Node searchNode(final NodeArray children, final CharSequence word, final int offset, final int length) { - // TODO Consider combining with addWordRec final int count = children.mLength; - char currentChar = word.charAt(offset); + final char currentChar = word.charAt(offset); for (int j = 0; j < count; j++) { final Node node = children.mData[j]; if (node.mCode == currentChar) { diff --git a/java/src/com/android/inputmethod/latin/Flag.java b/java/src/com/android/inputmethod/latin/Flag.java new file mode 100644 index 000000000..3cb8f7e17 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/Flag.java @@ -0,0 +1,64 @@ +/* + * 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.res.Resources; + +public class Flag { + public final String mName; + public final int mResource; + public final int mMask; + public final int mSource; + + static private final int SOURCE_CONFIG = 1; + static private final int SOURCE_EXTRAVALUE = 2; + + public Flag(int resourceId, int mask) { + mName = null; + mResource = resourceId; + mSource = SOURCE_CONFIG; + mMask = mask; + } + + public Flag(String name, int mask) { + mName = name; + mResource = 0; + mSource = SOURCE_EXTRAVALUE; + mMask = mask; + } + + // If context/switcher are null, set all related flags in flagArray to on. + public static int initFlags(Flag[] flagArray, Context context, SubtypeSwitcher switcher) { + int flags = 0; + final Resources res = null == context ? null : context.getResources(); + for (Flag entry : flagArray) { + switch (entry.mSource) { + case Flag.SOURCE_CONFIG: + if (res == null || res.getBoolean(entry.mResource)) + flags |= entry.mMask; + break; + case Flag.SOURCE_EXTRAVALUE: + if (switcher == null || + switcher.currentSubtypeContainsExtraValueKey(entry.mName)) + flags |= entry.mMask; + break; + } + } + return flags; + } +} diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java deleted file mode 100644 index b58a57e42..000000000 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2008-2009 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.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceGroup; -import android.preference.PreferenceManager; -import android.text.TextUtils; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -public class InputLanguageSelection extends PreferenceActivity { - - private SharedPreferences mPrefs; - private String mSelectedLanguages; - private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>(); - private static final String[] BLACKLIST_LANGUAGES = { - "ko", "ja", "zh", "el", "zz" - }; - - private static class Loc implements Comparable<Object> { - private static Collator sCollator = Collator.getInstance(); - - private String mLabel; - public final Locale mLocale; - - public Loc(String label, Locale locale) { - this.mLabel = label; - this.mLocale = locale; - } - - public void setLabel(String label) { - this.mLabel = label; - } - - @Override - public String toString() { - return this.mLabel; - } - - @Override - public int compareTo(Object o) { - return sCollator.compare(this.mLabel, ((Loc) o).mLabel); - } - } - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.language_prefs); - // Get the settings preferences - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - mSelectedLanguages = mPrefs.getString(Settings.PREF_SELECTED_LANGUAGES, ""); - String[] languageList = mSelectedLanguages.split(","); - mAvailableLanguages = getUniqueLocales(); - PreferenceGroup parent = getPreferenceScreen(); - for (int i = 0; i < mAvailableLanguages.size(); i++) { - CheckBoxPreference pref = new CheckBoxPreference(this); - Locale locale = mAvailableLanguages.get(i).mLocale; - pref.setTitle(SubtypeSwitcher.getFullDisplayName(locale, true)); - boolean checked = isLocaleIn(locale, languageList); - pref.setChecked(checked); - if (hasDictionary(locale)) { - pref.setSummary(R.string.has_dictionary); - } - parent.addPreference(pref); - } - } - - private boolean isLocaleIn(Locale locale, String[] list) { - String lang = get5Code(locale); - for (int i = 0; i < list.length; i++) { - if (lang.equalsIgnoreCase(list[i])) return true; - } - return false; - } - - private boolean hasDictionary(Locale locale) { - final Resources res = getResources(); - final Configuration conf = res.getConfiguration(); - final Locale saveLocale = conf.locale; - boolean haveDictionary = false; - conf.locale = locale; - res.updateConfiguration(conf, res.getDisplayMetrics()); - - 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 - // 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words. - if (bd.getSize() > Suggest.LARGE_DICTIONARY_THRESHOLD / 4) { - haveDictionary = true; - } - bd.close(); - conf.locale = saveLocale; - res.updateConfiguration(conf, res.getDisplayMetrics()); - return haveDictionary; - } - - private String get5Code(Locale locale) { - String country = locale.getCountry(); - return locale.getLanguage() - + (TextUtils.isEmpty(country) ? "" : "_" + country); - } - - @Override - protected void onResume() { - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - // Save the selected languages - String checkedLanguages = ""; - PreferenceGroup parent = getPreferenceScreen(); - int count = parent.getPreferenceCount(); - for (int i = 0; i < count; i++) { - CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i); - if (pref.isChecked()) { - Locale locale = mAvailableLanguages.get(i).mLocale; - checkedLanguages += get5Code(locale) + ","; - } - } - if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null - Editor editor = mPrefs.edit(); - editor.putString(Settings.PREF_SELECTED_LANGUAGES, checkedLanguages); - SharedPreferencesCompat.apply(editor); - } - - public ArrayList<Loc> getUniqueLocales() { - String[] locales = getAssets().getLocales(); - Arrays.sort(locales); - ArrayList<Loc> uniqueLocales = new ArrayList<Loc>(); - - final int origSize = locales.length; - Loc[] preprocess = new Loc[origSize]; - int finalSize = 0; - for (int i = 0 ; i < origSize; i++ ) { - String s = locales[i]; - int len = s.length(); - if (len == 5) { - String language = s.substring(0, 2); - String country = s.substring(3, 5); - Locale l = new Locale(language, country); - - // Exclude languages that are not relevant to LatinIME - if (arrayContains(BLACKLIST_LANGUAGES, language)) continue; - - if (finalSize == 0) { - preprocess[finalSize++] = - new Loc(SubtypeSwitcher.getFullDisplayName(l, true), l); - } else { - // check previous entry: - // same lang and a country -> upgrade to full name and - // insert ours with full name - // diff lang -> insert ours with lang-only name - if (preprocess[finalSize-1].mLocale.getLanguage().equals( - language)) { - preprocess[finalSize-1].setLabel(SubtypeSwitcher.getFullDisplayName( - preprocess[finalSize-1].mLocale, false)); - preprocess[finalSize++] = - new Loc(SubtypeSwitcher.getFullDisplayName(l, false), l); - } else { - String displayName; - if (s.equals("zz_ZZ")) { - // ignore this locale - } else { - displayName = SubtypeSwitcher.getFullDisplayName(l, true); - preprocess[finalSize++] = new Loc(displayName, l); - } - } - } - } - } - for (int i = 0; i < finalSize ; i++) { - uniqueLocales.add(preprocess[i]); - } - return uniqueLocales; - } - - private boolean arrayContains(String[] array, String value) { - for (int i = 0; i < array.length; i++) { - if (array[i].equalsIgnoreCase(value)) return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java deleted file mode 100644 index 2dd3038f9..000000000 --- a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2010 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.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.Locale; - -/** - * Keeps track of list of selected input languages and the current - * input language that the user has selected. - */ -public class LanguageSwitcher { - - private final ArrayList<Locale> mLocales = new ArrayList<Locale>(); - private final LatinIME mIme; - private String[] mSelectedLanguageArray; - private String mSelectedLanguages; - private int mCurrentIndex = 0; - private String mDefaultInputLanguage; - private Locale mDefaultInputLocale; - private Locale mSystemLocale; - - public LanguageSwitcher(LatinIME ime) { - mIme = ime; - } - - public int getLocaleCount() { - return mLocales.size(); - } - - /** - * Loads the currently selected input languages from shared preferences. - * @param sp - * @return whether there was any change - */ - public boolean loadLocales(SharedPreferences sp) { - String selectedLanguages = sp.getString(Settings.PREF_SELECTED_LANGUAGES, null); - String currentLanguage = sp.getString(Settings.PREF_INPUT_LANGUAGE, null); - if (selectedLanguages == null || selectedLanguages.length() < 1) { - loadDefaults(); - if (mLocales.size() == 0) { - return false; - } - mLocales.clear(); - return true; - } - if (selectedLanguages.equals(mSelectedLanguages)) { - return false; - } - mSelectedLanguageArray = selectedLanguages.split(","); - mSelectedLanguages = selectedLanguages; // Cache it for comparison later - constructLocales(); - mCurrentIndex = 0; - if (currentLanguage != null) { - // Find the index - mCurrentIndex = 0; - for (int i = 0; i < mLocales.size(); i++) { - if (mSelectedLanguageArray[i].equals(currentLanguage)) { - mCurrentIndex = i; - break; - } - } - // If we didn't find the index, use the first one - } - return true; - } - - private void loadDefaults() { - mDefaultInputLocale = mIme.getResources().getConfiguration().locale; - String country = mDefaultInputLocale.getCountry(); - mDefaultInputLanguage = mDefaultInputLocale.getLanguage() + - (TextUtils.isEmpty(country) ? "" : "_" + country); - } - - private void constructLocales() { - mLocales.clear(); - for (final String lang : mSelectedLanguageArray) { - final Locale locale = new Locale(lang.substring(0, 2), - lang.length() > 4 ? lang.substring(3, 5) : ""); - mLocales.add(locale); - } - } - - /** - * Returns the currently selected input language code, or the display language code if - * no specific locale was selected for input. - */ - public String getInputLanguage() { - if (getLocaleCount() == 0) return mDefaultInputLanguage; - - return mSelectedLanguageArray[mCurrentIndex]; - } - - /** - * Returns the list of enabled language codes. - */ - public String[] getEnabledLanguages() { - return mSelectedLanguageArray; - } - - /** - * Returns the currently selected input locale, or the display locale if no specific - * locale was selected for input. - * @return - */ - public Locale getInputLocale() { - if (getLocaleCount() == 0) return mDefaultInputLocale; - - return mLocales.get(mCurrentIndex); - } - - private int nextLocaleIndex() { - final int size = mLocales.size(); - return (mCurrentIndex + 1) % size; - } - - private int prevLocaleIndex() { - final int size = mLocales.size(); - return (mCurrentIndex - 1 + size) % size; - } - - /** - * Returns the next input locale in the list. Wraps around to the beginning of the - * list if we're at the end of the list. - * @return - */ - public Locale getNextInputLocale() { - if (getLocaleCount() == 0) return mDefaultInputLocale; - return mLocales.get(nextLocaleIndex()); - } - - /** - * Sets the system locale (display UI) used for comparing with the input language. - * @param locale the locale of the system - */ - public void setSystemLocale(Locale locale) { - mSystemLocale = locale; - } - - /** - * Returns the system locale. - * @return the system locale - */ - public Locale getSystemLocale() { - return mSystemLocale; - } - - /** - * Returns the previous input locale in the list. Wraps around to the end of the - * list if we're at the beginning of the list. - * @return - */ - public Locale getPrevInputLocale() { - if (getLocaleCount() == 0) return mDefaultInputLocale; - return mLocales.get(prevLocaleIndex()); - } - - public void reset() { - mCurrentIndex = 0; - } - - public void next() { - mCurrentIndex = nextLocaleIndex(); - } - - public void prev() { - mCurrentIndex = prevLocaleIndex(); - } - - public void persist(SharedPreferences prefs) { - Editor editor = prefs.edit(); - editor.putString(Settings.PREF_INPUT_LANGUAGE, getInputLanguage()); - SharedPreferencesCompat.apply(editor); - } -} diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index bf831bfbc..5b242b1e0 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -33,7 +33,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.SystemClock; -import android.os.Vibrator; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.text.InputType; @@ -42,46 +41,44 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; -import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; -import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; -import android.widget.LinearLayout; +import com.android.inputmethod.compat.CompatUtils; +import com.android.inputmethod.compat.EditorInfoCompatUtils; +import com.android.inputmethod.compat.InputConnectionCompatUtils; +import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; +import com.android.inputmethod.compat.InputTypeCompatUtils; +import com.android.inputmethod.compat.SuggestionSpanUtils; +import com.android.inputmethod.deprecated.LanguageSwitcherProxy; +import com.android.inputmethod.deprecated.VoiceProxy; +import com.android.inputmethod.deprecated.recorrection.Recorrection; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.LatinKeyboard; import com.android.inputmethod.keyboard.LatinKeyboardView; -import com.android.inputmethod.latin.Utils.RingCharBuffer; -import com.android.inputmethod.voice.VoiceIMEConnector; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Locale; /** * Input method implementation for Qwerty'ish keyboard. */ -public class LatinIME extends InputMethodService implements KeyboardActionListener { +public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener, + CandidateView.Listener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean PERF_DEBUG = false; private static final boolean TRACE = false; @@ -94,6 +91,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen * * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed. */ + @SuppressWarnings("dep-ann") public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm"; /** @@ -109,9 +107,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ 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; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; // How many continuous deletes at which to start deleting at a higher speed. @@ -119,6 +114,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Key events coming any faster than this are long-presses. private static final int QUICK_PRESS = 200; + /** + * The name of the scheme used by the Package Manager to warn of a new package installation, + * replacement or removal. + */ + private static final String SCHEME_PACKAGE = "package"; + private int mSuggestionVisibility; private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE = R.string.prefs_suggestion_visibility_show_value; @@ -133,55 +134,48 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen SUGGESTION_VISIBILILTY_HIDE_VALUE }; + private Settings.Values mSettingsValues; + private View mCandidateViewContainer; + private int mCandidateStripHeight; private CandidateView mCandidateView; private Suggest mSuggest; private CompletionInfo[] mApplicationSpecifiedCompletions; private AlertDialog mOptionsDialog; - private InputMethodManager mImm; + private InputMethodManagerCompatWrapper mImm; private Resources mResources; private SharedPreferences mPrefs; private String mInputMethodId; private KeyboardSwitcher mKeyboardSwitcher; private SubtypeSwitcher mSubtypeSwitcher; - private VoiceIMEConnector mVoiceConnector; + private VoiceProxy mVoiceProxy; + private Recorrection mRecorrection; private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private ContactsDictionary mContactsDictionary; private AutoDictionary mAutoDictionary; + // TODO: Create an inner class to group options and pseudo-options to improve readability. // These variables are initialized according to the {@link EditorInfo#inputType}. - private boolean mAutoSpace; + private boolean mShouldInsertMagicSpace; private boolean mInputTypeNoAutoCorrect; private boolean mIsSettingsSuggestionStripOn; private boolean mApplicationSpecifiedCompletionOn; - private AccessibilityUtils mAccessibilityUtils; - private final StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); private CharSequence mBestWord; - private boolean mHasValidSuggestions; + private boolean mHasUncommittedTypedChars; private boolean mHasDictionary; - private boolean mJustAddedAutoSpace; - private boolean mAutoCorrectEnabled; - private boolean mRecorrectionEnabled; - private boolean mBigramSuggestionEnabled; - private boolean mAutoCorrectOn; - private boolean mVibrateOn; - private boolean mSoundOn; - private boolean mPopupOn; - private boolean mAutoCap; - private boolean mQuickFixes; - private boolean mConfigEnableShowSubtypeSettings; - private boolean mConfigSwipeDownDismissKeyboardEnabled; - private int mConfigDelayBeforeFadeoutLanguageOnSpacebar; - private int mConfigDurationOfFadeoutLanguageOnSpacebar; - private float mConfigFinalFadeoutFactorOfLanguageOnSpacebar; - private long mConfigDoubleSpacesTurnIntoPeriodTimeout; + // Magic space: a space that should disappear on space/apostrophe insertion, move after the + // punctuation on punctuation insertion, and become a real space on alpha char insertion. + private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space. + // This indicates whether the last keypress resulted in processing of double space replacement + // with period-space. + private boolean mJustReplacedDoubleSpace; private int mCorrectionMode; private int mCommittedLength; @@ -189,76 +183,28 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Keep track of the last selection range to decide if we need to show word alternatives private int mLastSelectionStart; private int mLastSelectionEnd; - private SuggestedWords mSuggestPuncList; - // Indicates whether the suggestion strip is to be on in landscape - private boolean mJustAccepted; + // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't + // "expect" it, it means the user actually moved the cursor. + private boolean mExpectingUpdateSelection; private int mDeleteCount; private long mLastKeyTime; private AudioManager mAudioManager; // Align sound effect volume on music volume private static final float FX_VOLUME = -1.0f; - private boolean mSilentMode; + private boolean mSilentModeOn; // System-wide current configuration - /* package */ String mWordSeparators; - private String mSentenceSeparators; - private String mSuggestPuncs; - // TODO: Move this flag to VoiceIMEConnector + // TODO: Move this flag to VoiceProxy private boolean mConfigurationChanging; + // Object for reacting to adding/removing a dictionary pack. + private BroadcastReceiver mDictionaryPackInstallReceiver = + new DictionaryPackInstallBroadcastReceiver(this); + // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; - private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); - - public abstract static class WordAlternatives { - protected CharSequence mChosenWord; - - public WordAlternatives() { - // Nothing - } - - public WordAlternatives(CharSequence chosenWord) { - mChosenWord = chosenWord; - } - - @Override - public int hashCode() { - return mChosenWord.hashCode(); - } - - public abstract CharSequence getOriginalWord(); - - public CharSequence getChosenWord() { - return mChosenWord; - } - - public abstract SuggestedWords.Builder getAlternatives(); - } - - public class TypedWordAlternatives extends WordAlternatives { - private WordComposer word; - - public TypedWordAlternatives() { - // Nothing - } - - public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) { - super(chosenWord); - word = wordComposer; - } - - @Override - public CharSequence getOriginalWord() { - return word.getTypedWord(); - } - - @Override - public SuggestedWords.Builder getAlternatives() { - return getTypedSuggestions(word); - } - } public final UIHandler mHandler = new UIHandler(); @@ -270,44 +216,54 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen 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; + private static final int MSG_SET_BIGRAM_PREDICTIONS = 7; @Override public void handleMessage(Message msg) { final KeyboardSwitcher switcher = mKeyboardSwitcher; - final LatinKeyboardView inputView = switcher.getInputView(); + final LatinKeyboardView inputView = switcher.getKeyboardView(); switch (msg.what) { case MSG_UPDATE_SUGGESTIONS: updateSuggestions(); break; case MSG_UPDATE_OLD_SUGGESTIONS: - setOldSuggestions(); + mRecorrection.fetchAndDisplayRecorrectionSuggestions(mVoiceProxy, mCandidateView, + mSuggest, mKeyboardSwitcher, mWord, mHasUncommittedTypedChars, + mLastSelectionStart, mLastSelectionEnd, mSettingsValues.mWordSeparators); break; case MSG_UPDATE_SHIFT_STATE: switcher.updateShiftState(); break; + case MSG_SET_BIGRAM_PREDICTIONS: + updateBigramPredictions(); + break; case MSG_VOICE_RESULTS: - mVoiceConnector.handleVoiceResults(preferCapitalization() + mVoiceProxy.handleVoiceResults(preferCapitalization() || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked())); break; case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: - if (inputView != null) + if (inputView != null) { inputView.setSpacebarTextFadeFactor( - (1.0f + mConfigFinalFadeoutFactorOfLanguageOnSpacebar) / 2, + (1.0f + mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar) / 2, (LatinKeyboard)msg.obj); + } sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), - mConfigDurationOfFadeoutLanguageOnSpacebar); + mSettingsValues.mDurationOfFadeoutLanguageOnSpacebar); break; case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: - if (inputView != null) + if (inputView != null) { inputView.setSpacebarTextFadeFactor( - mConfigFinalFadeoutFactorOfLanguageOnSpacebar, (LatinKeyboard)msg.obj); + mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar, + (LatinKeyboard)msg.obj); + } break; } } public void postUpdateSuggestions() { removeMessages(MSG_UPDATE_SUGGESTIONS); - sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), + mSettingsValues.mDelayUpdateSuggestions); } public void cancelUpdateSuggestions() { @@ -321,7 +277,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void postUpdateOldSuggestions() { removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), - DELAY_UPDATE_OLD_SUGGESTIONS); + mSettingsValues.mDelayUpdateOldSuggestions); } public void cancelUpdateOldSuggestions() { @@ -330,13 +286,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void postUpdateShiftKeyState() { removeMessages(MSG_UPDATE_SHIFT_STATE); - sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), + mSettingsValues.mDelayUpdateShiftState); } public void cancelUpdateShiftState() { removeMessages(MSG_UPDATE_SHIFT_STATE); } + public void postUpdateBigramPredictions() { + removeMessages(MSG_SET_BIGRAM_PREDICTIONS); + sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), + mSettingsValues.mDelayUpdateSuggestions); + } + + public void cancelUpdateBigramPredictions() { + removeMessages(MSG_SET_BIGRAM_PREDICTIONS); + } + public void updateVoiceResults() { sendMessage(obtainMessage(MSG_VOICE_RESULTS)); } @@ -344,17 +311,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void startDisplayLanguageOnSpacebar(boolean localeChanged) { removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); - final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + final LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) { final LatinKeyboard keyboard = mKeyboardSwitcher.getLatinKeyboard(); - // The language is never displayed when the delay is zero. - if (mConfigDelayBeforeFadeoutLanguageOnSpacebar != 0) - inputView.setSpacebarTextFadeFactor(localeChanged ? 1.0f - : mConfigFinalFadeoutFactorOfLanguageOnSpacebar, keyboard); // The language is always displayed when the delay is negative. - if (localeChanged && mConfigDelayBeforeFadeoutLanguageOnSpacebar > 0) { + final boolean needsToDisplayLanguage = localeChanged + || mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar < 0; + // The language is never displayed when the delay is zero. + if (mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar != 0) { + inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f + : mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar, keyboard); + } + // The fadeout animation will start when the delay is positive. + if (localeChanged && mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar > 0) { sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard), - mConfigDelayBeforeFadeoutLanguageOnSpacebar); + mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar); } } } @@ -362,7 +333,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void startDoubleSpacesTimer() { removeMessages(MSG_SPACE_TYPED); sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), - mConfigDoubleSpacesTurnIntoPeriodTimeout); + mSettingsValues.mDoubleSpacesTurnIntoPeriodTimeout); } public void cancelDoubleSpacesTimer() { @@ -379,44 +350,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs = prefs; LatinImeLogger.init(this, prefs); + LanguageSwitcherProxy.init(this, prefs); SubtypeSwitcher.init(this, prefs); KeyboardSwitcher.init(this, prefs); - AccessibilityUtils.init(this, prefs); + Recorrection.init(this, prefs); super.onCreate(); - mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); + mImm = InputMethodManagerCompatWrapper.getInstance(this); mInputMethodId = Utils.getInputMethodId(mImm, getPackageName()); mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); - mAccessibilityUtils = AccessibilityUtils.getInstance(); + mRecorrection = Recorrection.getInstance(); DEBUG = LatinImeLogger.sDBG; + loadSettings(); + final Resources res = getResources(); mResources = res; - // 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.config_default_recorrection_enabled)); - } else { - mRecorrectionEnabled = res.getBoolean(R.bool.config_default_recorrection_enabled); - } - - mConfigEnableShowSubtypeSettings = res.getBoolean( - R.bool.config_enable_show_subtype_settings); - mConfigSwipeDownDismissKeyboardEnabled = res.getBoolean( - R.bool.config_swipe_down_dismiss_keyboard_enabled); - mConfigDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( - R.integer.config_delay_before_fadeout_language_on_spacebar); - mConfigDurationOfFadeoutLanguageOnSpacebar = res.getInteger( - 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; for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { @@ -429,49 +381,73 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mOrientation = res.getConfiguration().orientation; - initSuggestPuncList(); - // register to receive ringer mode change and network state change. + // Register to receive ringer mode change and network state change. + // Also receive installation and removal of a dictionary pack. final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); registerReceiver(mReceiver, filter); - mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); + mVoiceProxy = VoiceProxy.init(this, prefs, mHandler); + + final IntentFilter packageFilter = new IntentFilter(); + packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageFilter.addDataScheme(SCHEME_PACKAGE); + registerReceiver(mDictionaryPackInstallReceiver, packageFilter); + + final IntentFilter newDictFilter = new IntentFilter(); + newDictFilter.addAction( + DictionaryPackInstallBroadcastReceiver.NEW_DICTIONARY_INTENT_ACTION); + registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + } + + // Has to be package-visible for unit tests + /* package */ void loadSettings() { + if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance(); + mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr()); } private void initSuggest() { - String locale = mSubtypeSwitcher.getInputLocaleStr(); + final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); + final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr); - Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); + final Resources res = mResources; + final Locale savedLocale = Utils.setSystemLocale(res, keyboardLocale); if (mSuggest != null) { mSuggest.close(); } - final SharedPreferences prefs = mPrefs; - mQuickFixes = isQuickFixesEnabled(prefs); - final Resources res = mResources; int mainDicResId = Utils.getMainDictionaryResourceId(res); - mSuggest = new Suggest(this, mainDicResId); - loadAndSetAutoCorrectionThreshold(prefs); + mSuggest = new Suggest(this, mainDicResId, keyboardLocale); + if (mSettingsValues.mAutoCorrectEnabled) { + mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); + } updateAutoTextEnabled(); - mUserDictionary = new UserDictionary(this, locale); + mUserDictionary = new UserDictionary(this, localeStr); mSuggest.setUserDictionary(mUserDictionary); mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); mSuggest.setContactsDictionary(mContactsDictionary); - mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO); + mAutoDictionary = new AutoDictionary(this, this, localeStr, Suggest.DIC_AUTO); mSuggest.setAutoDictionary(mAutoDictionary); - mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER); + mUserBigramDictionary = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER); mSuggest.setUserBigramDictionary(mUserBigramDictionary); updateCorrectionMode(); - mWordSeparators = res.getString(R.string.word_separators); - mSentenceSeparators = res.getString(R.string.sentence_separators); - mSubtypeSwitcher.changeSystemLocale(savedLocale); + Utils.setSystemLocale(res, savedLocale); + } + + /* package private */ void resetSuggestMainDict() { + final String localeStr = mSubtypeSwitcher.getInputLocaleStr(); + final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr); + int mainDicResId = Utils.getMainDictionaryResourceId(mResources); + mSuggest.resetMainDict(this, mainDicResId, keyboardLocale); } @Override @@ -481,7 +457,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSuggest = null; } unregisterReceiver(mReceiver); - mVoiceConnector.destroy(); + unregisterReceiver(mDictionaryPackInstallReceiver); + mVoiceProxy.destroy(); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); super.onDestroy(); @@ -502,8 +479,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mConfigurationChanging = true; super.onConfigurationChanged(conf); - mVoiceConnector.onConfigurationChanged(conf); + mVoiceProxy.onConfigurationChanged(conf); mConfigurationChanging = false; + + // This will work only when the subtype is not supported. + LanguageSwitcherProxy.onConfigurationChanged(conf); } @Override @@ -512,26 +492,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public View onCreateCandidatesView() { - LayoutInflater inflater = getLayoutInflater(); - LinearLayout container = (LinearLayout)inflater.inflate(R.layout.candidates, null); - mCandidateViewContainer = container; - if (container.getPaddingRight() != 0) { - HorizontalScrollView scrollView = - (HorizontalScrollView) container.findViewById(R.id.candidates_scroll_view); - scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); - container.setGravity(Gravity.CENTER_HORIZONTAL); - } - mCandidateView = (CandidateView) container.findViewById(R.id.candidates); - mCandidateView.setService(this); - setCandidatesViewShown(true); - return container; + public void setInputView(View view) { + super.setInputView(view); + mCandidateViewContainer = view.findViewById(R.id.candidates_container); + mCandidateView = (CandidateView) view.findViewById(R.id.candidates); + mCandidateView.setListener(this); + mCandidateStripHeight = (int)mResources.getDimension(R.dimen.candidate_strip_height); + } + + @Override + public void setCandidatesView(View view) { + // To ensure that CandidatesView will never be set. + return; } @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { final KeyboardSwitcher switcher = mKeyboardSwitcher; - LatinKeyboardView inputView = switcher.getInputView(); + LatinKeyboardView inputView = switcher.getKeyboardView(); if (DEBUG) { Log.d(TAG, "onStartInputView: inputType=" + ((attribute == null) ? "none" @@ -549,20 +527,32 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to // know now whether this is a password text field, because we need to know now whether we // want to enable the voice button. - final VoiceIMEConnector voiceIme = mVoiceConnector; - voiceIme.resetVoiceStates(Utils.isPasswordInputType(attribute.inputType) - || Utils.isVisiblePasswordInputType(attribute.inputType)); + final VoiceProxy voiceIme = mVoiceProxy; + voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(attribute.inputType) + || InputTypeCompatUtils.isVisiblePasswordInputType(attribute.inputType)); initializeInputAttributes(attribute); inputView.closing(); mEnteredText = null; mComposing.setLength(0); - mHasValidSuggestions = false; + mHasUncommittedTypedChars = false; mDeleteCount = 0; - mJustAddedAutoSpace = false; + mJustAddedMagicSpace = false; + mJustReplacedDoubleSpace = false; + + loadSettings(); + updateCorrectionMode(); + updateAutoTextEnabled(); + updateSuggestionVisibility(mPrefs, mResources); + + if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { + mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); + } + mVoiceProxy.loadSettings(attribute, mPrefs); + // This will work only when the subtype is not supported. + LanguageSwitcherProxy.loadSettings(); - loadSettings(attribute); if (mSubtypeSwitcher.isKeyboardMode()) { switcher.loadKeyboard(attribute, mSubtypeSwitcher.isShortcutImeEnabled() && voiceIme.isVoiceButtonEnabled(), @@ -570,20 +560,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen switcher.updateShiftState(); } - setCandidatesViewShownInternal(isCandidateStripVisible(), - false /* needsInputViewShown */ ); + setSuggestionStripShownInternal(isCandidateStripVisible(), /* needsInputViewShown */ false); // Delay updating suggestions because keyboard input view may not be shown at this point. mHandler.postUpdateSuggestions(); updateCorrectionMode(); - final boolean accessibilityEnabled = mAccessibilityUtils.isAccessibilityEnabled(); - - inputView.setPreviewEnabled(mPopupOn); + inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, + mSettingsValues.mKeyPreviewPopupDismissDelay); inputView.setProximityCorrectionEnabled(true); - inputView.setAccessibilityEnabled(accessibilityEnabled); // If we just entered a text field, maybe it has some old text that requires correction - checkRecorrectionOnStart(); + mRecorrection.checkRecorrectionOnStart(); inputView.setForeground(true); voiceIme.onStartInputView(inputView.getWindowToken()); @@ -596,7 +583,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; final int inputType = attribute.inputType; final int variation = inputType & InputType.TYPE_MASK_VARIATION; - mAutoSpace = false; + mShouldInsertMagicSpace = false; mInputTypeNoAutoCorrect = false; mIsSettingsSuggestionStripOn = false; mApplicationSpecifiedCompletionOn = false; @@ -605,17 +592,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen 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)) { + if (InputTypeCompatUtils.isPasswordInputType(inputType) + || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) { mIsSettingsSuggestionStripOn = false; } - if (Utils.isEmailVariation(variation) + if (InputTypeCompatUtils.isEmailVariation(variation) || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { - mAutoSpace = false; + mShouldInsertMagicSpace = false; } else { - mAutoSpace = true; + mShouldInsertMagicSpace = true; } - if (Utils.isEmailVariation(variation)) { + if (InputTypeCompatUtils.isEmailVariation(variation)) { mIsSettingsSuggestionStripOn = false; } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { mIsSettingsSuggestionStripOn = false; @@ -646,32 +633,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void checkRecorrectionOnStart() { - if (!mRecorrectionEnabled) return; - - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; - // There could be a pending composing span. Clean it up first. - ic.finishComposingText(); - - if (isShowingSuggestionsStrip() && isSuggestionsRequested()) { - // First get the cursor position. This is required by setOldSuggestions(), so that - // it can pass the correct range to setComposingRegion(). At this point, we don't - // have valid values for mLastSelectionStart/End because onUpdateSelection() has - // not been called yet. - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; // anything is fine here - ExtractedText et = ic.getExtractedText(etr, 0); - if (et == null) return; - - mLastSelectionStart = et.startOffset + et.selectionStart; - mLastSelectionEnd = et.startOffset + et.selectionEnd; - - // Then look for possible corrections in a delayed fashion - if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) { - mHandler.postUpdateOldSuggestions(); - } - } + @Override + public void onWindowHidden() { + super.onWindowHidden(); + KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); + if (inputView != null) inputView.closing(); } @Override @@ -681,9 +647,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen LatinImeLogger.commit(); mKeyboardSwitcher.onAutoCorrectionStateChanged(false); - mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging); + mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging); - KeyboardView inputView = mKeyboardSwitcher.getInputView(); + KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); @@ -692,7 +658,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onFinishInputView(boolean finishingInput) { super.onFinishInputView(finishingInput); - KeyboardView inputView = mKeyboardSwitcher.getInputView(); + KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.setForeground(false); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestions(); @@ -702,7 +668,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onUpdateExtractedText(int token, ExtractedText text) { super.onUpdateExtractedText(token, text); - mVoiceConnector.showPunctuationHintIfNecessary(); + mVoiceProxy.showPunctuationHintIfNecessary(); } @Override @@ -723,67 +689,62 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ", ce=" + candidatesEnd); } - mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart); + mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart); // If the current selection in the text view changes, we should // clear whatever candidate text we have. final boolean selectionChanged = (newSelStart != candidatesEnd || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart; final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1; - if (((mComposing.length() > 0 && mHasValidSuggestions) - || mVoiceConnector.isVoiceInputHighlighted()) + if (((mComposing.length() > 0 && mHasUncommittedTypedChars) + || mVoiceProxy.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); + mRecorrection.saveRecorrectionSuggestion(mWord, mComposing); } mComposing.setLength(0); - mHasValidSuggestions = false; - mHandler.postUpdateSuggestions(); + mHasUncommittedTypedChars = false; + if (isCursorTouchingWord()) { + mHandler.cancelUpdateBigramPredictions(); + mHandler.postUpdateSuggestions(); + } else { + setPunctuationSuggestions(); + } TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.finishComposingText(); } - mVoiceConnector.setVoiceInputHighlighted(false); - } else if (!mHasValidSuggestions && !mJustAccepted) { + mVoiceProxy.setVoiceInputHighlighted(false); + } else if (!mHasUncommittedTypedChars && !mExpectingUpdateSelection) { if (TextEntryState.isAcceptedDefault() || TextEntryState.isSpaceAfterPicked()) { if (TextEntryState.isAcceptedDefault()) TextEntryState.reset(); - mJustAddedAutoSpace = false; // The user moved the cursor. } } - mJustAccepted = false; + if (!mExpectingUpdateSelection) { + mJustAddedMagicSpace = false; // The user moved the cursor. + mJustReplacedDoubleSpace = false; + } + mExpectingUpdateSelection = false; mHandler.postUpdateShiftKeyState(); // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; - 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() - && (candidatesStart == candidatesEnd || newSelStart != oldSelStart - || TextEntryState.isRecorrecting()) - && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) { - if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { - mHandler.postUpdateOldSuggestions(); - } else { - 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() - && !mCandidateView.isShowingAddToDictionaryHint()) { - setPunctuationSuggestions(); - } - } - } - } - } + mRecorrection.updateRecorrectionSelection(mKeyboardSwitcher, + mCandidateView, candidatesStart, candidatesEnd, newSelStart, + newSelEnd, oldSelStart, mLastSelectionStart, + mLastSelectionEnd, mHasUncommittedTypedChars); + } + + public void setLastSelection(int start, int end) { + mLastSelectionStart = start; + mLastSelectionEnd = end; } /** @@ -796,7 +757,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedTextClicked() { - if (mRecorrectionEnabled && isSuggestionsRequested()) return; + if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return; super.onExtractedTextClicked(); } @@ -812,7 +773,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedCursorMovement(int dx, int dy) { - if (mRecorrectionEnabled && isSuggestionsRequested()) return; + if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return; super.onExtractedCursorMovement(dx, dy); } @@ -827,8 +788,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mOptionsDialog.dismiss(); mOptionsDialog = null; } - mVoiceConnector.hideVoiceWindow(mConfigurationChanging); - mWordHistory.clear(); + mVoiceProxy.hideVoiceWindow(mConfigurationChanging); + mRecorrection.clearWordsInHistory(); super.hideWindow(); } @@ -856,57 +817,58 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // When in fullscreen mode, show completions generated by the application setSuggestions(builder.build()); mBestWord = null; - setCandidatesViewShown(true); + setSuggestionStripShown(true); } } - private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) { - // TODO: Remove this if we support candidates with hard keyboard + private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) { + // TODO: Modify this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { - super.setCandidatesViewShown(shown - && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true)); + final boolean shouldShowCandidates = shown + && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true); + if (isExtractViewShown()) { + // No need to have extra space to show the key preview. + mCandidateViewContainer.setMinimumHeight(0); + mCandidateViewContainer.setVisibility( + shouldShowCandidates ? View.VISIBLE : View.GONE); + } else { + // We must control the visibility of the suggestion strip in order to avoid clipped + // key previews, even when we don't show the suggestion strip. + mCandidateViewContainer.setVisibility( + shouldShowCandidates ? View.VISIBLE : View.INVISIBLE); + } } } - @Override - public void setCandidatesViewShown(boolean shown) { - setCandidatesViewShownInternal(shown, true /* needsInputViewShown */ ); + private void setSuggestionStripShown(boolean shown) { + setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); } @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); - if (!isFullscreenMode()) { - outInsets.contentTopInsets = outInsets.visibleTopInsets; - } - KeyboardView inputView = mKeyboardSwitcher.getInputView(); + final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); + if (inputView == null) + return; + final int containerHeight = mCandidateViewContainer.getHeight(); + int touchY = containerHeight; // Need to set touchable region only if input view is being shown - if (inputView != null && mKeyboardSwitcher.isInputViewShown()) { - final int x = 0; - int y = 0; - final int width = inputView.getWidth(); - int height = inputView.getHeight() + EXTENDED_TOUCHABLE_REGION_HEIGHT; - if (mCandidateViewContainer != null) { - ViewParent candidateParent = mCandidateViewContainer.getParent(); - if (candidateParent instanceof FrameLayout) { - FrameLayout fl = (FrameLayout) candidateParent; - if (fl != null) { - // Check frame layout's visibility - if (fl.getVisibility() == View.INVISIBLE) { - y = fl.getHeight(); - height += y; - } else if (fl.getVisibility() == View.VISIBLE) { - height += fl.getHeight(); - } - } - } + if (mKeyboardSwitcher.isInputViewShown()) { + if (mCandidateViewContainer.getVisibility() == View.VISIBLE) { + touchY -= mCandidateStripHeight; } + final int touchWidth = inputView.getWidth(); + final int touchHeight = inputView.getHeight() + containerHeight + // Extend touchable region below the keyboard. + + EXTENDED_TOUCHABLE_REGION_HEIGHT; if (DEBUG) { - Log.d(TAG, "Touchable region " + x + ", " + y + ", " + width + ", " + height); + Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth + + " height=" + touchHeight); } - outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; - outInsets.touchableRegion.set(x, y, width, height); + setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight); } + outInsets.contentTopInsets = touchY; + outInsets.visibleTopInsets = touchY; } @Override @@ -927,8 +889,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: - if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { - if (mKeyboardSwitcher.getInputView().handleBack()) { + if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getKeyboardView() != null) { + if (mKeyboardSwitcher.getKeyboardView().handleBack()) { return true; } } @@ -962,8 +924,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void commitTyped(InputConnection inputConnection) { - if (mHasValidSuggestions) { - mHasValidSuggestions = false; + if (mHasUncommittedTypedChars) { + mHasUncommittedTypedChars = false; if (mComposing.length() > 0) { if (inputConnection != null) { inputConnection.commitText(mComposing, 1); @@ -979,45 +941,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public boolean getCurrentAutoCapsState() { InputConnection ic = getCurrentInputConnection(); EditorInfo ei = getCurrentInputEditorInfo(); - if (mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) { + if (mSettingsValues.mAutoCap && ic != null && ei != null + && ei.inputType != InputType.TYPE_NULL) { return ic.getCursorCapsMode(ei.inputType) != 0; } return false; } - private void swapPunctuationAndSpace() { + private void swapSwapperAndSpace() { final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); + // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. if (lastTwo != null && lastTwo.length() == 2 - && lastTwo.charAt(0) == Keyboard.CODE_SPACE - && isSentenceSeparator(lastTwo.charAt(1))) { + && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); - mJustAddedAutoSpace = true; } } - private void reswapPeriodAndSpace() { - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; - CharSequence lastThree = ic.getTextBeforeCursor(3, 0); - if (lastThree != null && lastThree.length() == 3 - && lastThree.charAt(0) == Keyboard.CODE_PERIOD - && lastThree.charAt(1) == Keyboard.CODE_SPACE - && lastThree.charAt(2) == Keyboard.CODE_PERIOD) { - ic.beginBatchEdit(); - ic.deleteSurroundingText(3, 0); - ic.commitText(" ..", 1); - ic.endBatchEdit(); - mKeyboardSwitcher.updateShiftState(); - } - } - - private void doubleSpace() { + private void maybeDoubleSpace() { if (mCorrectionMode == Suggest.CORRECTION_NONE) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; @@ -1033,7 +979,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ic.commitText(". ", 1); ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); - mJustAddedAutoSpace = true; + mJustReplacedDoubleSpace = true; } else { mHandler.startDoubleSpacesTimer(); } @@ -1064,6 +1010,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } + @Override public boolean addWordToDictionary(String word) { mUserDictionary.addWord(word, 128); // Suggestion strip should be updated after the operation of adding word to the @@ -1081,14 +1028,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void onSettingsKeyPressed() { - if (!isShowingOptionDialog()) { - if (!mConfigEnableShowSubtypeSettings) { - showSubtypeSelectorAndSettings(); - } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { - showOptionsMenu(); - } else { - launchSettings(); - } + if (isShowingOptionDialog()) + return; + if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { + showSubtypeSelectorAndSettings(); + } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { + showOptionsMenu(); + } else { + launchSettings(); } } @@ -1115,22 +1062,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mLastKeyTime = when; KeyboardSwitcher switcher = mKeyboardSwitcher; - final boolean accessibilityEnabled = switcher.isAccessibilityEnabled(); final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); + final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace; + mJustReplacedDoubleSpace = false; switch (primaryCode) { case Keyboard.CODE_DELETE: - handleBackspace(); + handleBackspace(lastStateOfJustReplacedDoubleSpace); mDeleteCount++; + mExpectingUpdateSelection = true; LatinImeLogger.logOnDelete(); break; case Keyboard.CODE_SHIFT: // Shift key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch || accessibilityEnabled) + if (!distinctMultiTouch) switcher.toggleShift(); break; case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: // Symbol key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch || accessibilityEnabled) + if (!distinctMultiTouch) switcher.changeKeyboardMode(); break; case Keyboard.CODE_CANCEL: @@ -1144,32 +1093,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case Keyboard.CODE_SETTINGS_LONGPRESS: onSettingsKeyLongPressed(); break; - case Keyboard.CODE_NEXT_LANGUAGE: - toggleLanguage(false, true); + case LatinKeyboard.CODE_NEXT_LANGUAGE: + toggleLanguage(true); break; - case Keyboard.CODE_PREV_LANGUAGE: - toggleLanguage(false, false); + case LatinKeyboard.CODE_PREV_LANGUAGE: + toggleLanguage(false); break; case Keyboard.CODE_CAPSLOCK: switcher.toggleCapsLock(); break; - case Keyboard.CODE_VOICE: + case Keyboard.CODE_SHORTCUT: mSubtypeSwitcher.switchToShortcutIME(); break; case Keyboard.CODE_TAB: handleTab(); + // There are two cases for tab. Either we send a "next" event, that may change the + // focus but will never move the cursor. Or, we send a real tab keycode, which some + // applications may accept or ignore, and we don't know whether this will move the + // cursor or not. So actually, we don't really know. + // So to go with the safer option, we'd rather behave as if the user moved the + // cursor when they didn't than the opposite. We also expect that most applications + // will actually use tab only for focus movement. + // To sum it up: do not update mExpectingUpdateSelection here. break; default: - if (primaryCode != Keyboard.CODE_ENTER) { - mJustAddedAutoSpace = false; - } - RingCharBuffer.getInstance().push((char)primaryCode, x, y); - LatinImeLogger.logOnInputChar(); - if (isWordSeparator(primaryCode)) { - handleSeparator(primaryCode); + if (mSettingsValues.isWordSeparator(primaryCode)) { + handleSeparator(primaryCode, x, y); } else { handleCharacter(primaryCode, keyCodes, x, y); } + mExpectingUpdateSelection = true; + break; } switcher.onKey(primaryCode); // Reset after any single keystroke @@ -1178,10 +1132,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onTextInput(CharSequence text) { - mVoiceConnector.commitVoiceInput(); + mVoiceProxy.commitVoiceInput(); InputConnection ic = getCurrentInputConnection(); if (ic == null) return; - abortRecorrection(false); + mRecorrection.abortRecorrection(false); ic.beginBatchEdit(); commitTyped(ic); maybeRemovePreviousPeriod(text); @@ -1189,7 +1143,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); - mJustAddedAutoSpace = false; + mJustAddedMagicSpace = false; mEnteredText = text; } @@ -1199,26 +1153,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.onCancelInput(); } - private void handleBackspace() { - if (mVoiceConnector.logAndRevertVoiceInput()) return; + private void handleBackspace(boolean justReplacedDoubleSpace) { + if (mVoiceProxy.logAndRevertVoiceInput()) return; final InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); - mVoiceConnector.handleBackspace(); + mVoiceProxy.handleBackspace(); boolean deleteChar = false; - if (mHasValidSuggestions) { + if (mHasUncommittedTypedChars) { final int length = mComposing.length(); if (length > 0) { mComposing.delete(length - 1, length); mWord.deleteLast(); ic.setComposingText(mComposing, 1); if (mComposing.length() == 0) { - mHasValidSuggestions = false; + mHasUncommittedTypedChars = false; + } + if (1 == length) { + // 1 == length means we are about to erase the last character of the word, + // so we can show bigrams. + mHandler.postUpdateBigramPredictions(); + } else { + // length > 1, so we still have letters to deduce a suggestion from. + mHandler.postUpdateSuggestions(); } - mHandler.postUpdateSuggestions(); } else { ic.deleteSurroundingText(1, 0); } @@ -1233,6 +1194,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ic.endBatchEdit(); return; } + if (justReplacedDoubleSpace) { + if (revertDoubleSpace()) { + ic.endBatchEdit(); + return; + } + } if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { ic.deleteSurroundingText(mEnteredText.length(), 0); @@ -1258,9 +1225,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void handleTab() { final int imeOptions = getCurrentInputEditorInfo().imeOptions; - final int navigationFlags = - EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; - if ((imeOptions & navigationFlags) == 0) { + if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions) + && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) { sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); return; } @@ -1271,37 +1237,32 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // True if keyboard is in either chording shift or manual temporary upper case mode. final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); - if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 + if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions) && !isManualTemporaryUpperCase) { - ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); - } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 + EditorInfoCompatUtils.performEditorActionNext(ic); + } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions) && isManualTemporaryUpperCase) { - ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); - } - } - - private void abortRecorrection(boolean force) { - if (force || TextEntryState.isRecorrecting()) { - TextEntryState.onAbortRecorrection(); - setCandidatesViewShown(isCandidateStripVisible()); - getCurrentInputConnection().finishComposingText(); - clearSuggestions(); + EditorInfoCompatUtils.performEditorActionPrevious(ic); } } private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) { - mVoiceConnector.handleCharacter(); + mVoiceProxy.handleCharacter(); - if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isRecorrecting()) { - abortRecorrection(false); + if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) { + removeTrailingSpace(); + } + + if (mLastSelectionStart == mLastSelectionEnd) { + mRecorrection.abortRecorrection(false); } int code = primaryCode; if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) { - if (!mHasValidSuggestions) { - mHasValidSuggestions = true; + if (!mHasUncommittedTypedChars) { + mHasUncommittedTypedChars = true; mComposing.setLength(0); - saveWordInHistory(mBestWord); + mRecorrection.saveRecorrectionSuggestion(mWord, mBestWord); mWord.reset(); clearSuggestions(); } @@ -1325,7 +1286,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (mHasValidSuggestions) { + if (mHasUncommittedTypedChars) { if (mComposing.length() == 0 && switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()) { mWord.setFirstCharCapitalized(true); @@ -1344,16 +1305,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { sendKeyChar((char)code); } + if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { + swapSwapperAndSpace(); + } else { + mJustAddedMagicSpace = false; + } + switcher.updateShiftState(); if (LatinIME.PERF_DEBUG) measureCps(); - TextEntryState.typedCharacter((char) code, isWordSeparator(code)); + TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y); } - private void handleSeparator(int primaryCode) { - mVoiceConnector.handleSeparator(); + private void handleSeparator(int primaryCode, int x, int y) { + mVoiceProxy.handleSeparator(); // Should dismiss the "Touch again to save" message when handling separator if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { + mHandler.cancelUpdateBigramPredictions(); mHandler.postUpdateSuggestions(); } @@ -1362,54 +1330,61 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); - abortRecorrection(false); + mRecorrection.abortRecorrection(false); } - if (mHasValidSuggestions) { + if (mHasUncommittedTypedChars) { // 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 != '\'') { - pickedDefault = pickDefaultSuggestion(); - // Picked the suggestion by the space key. We consider this - // as "added an auto space". - if (primaryCode == Keyboard.CODE_SPACE) { - mJustAddedAutoSpace = true; - } + final boolean shouldAutoCorrect = + (mSettingsValues.mAutoCorrectEnabled || mSettingsValues.mQuickFixes) + && !mInputTypeNoAutoCorrect && mHasDictionary; + if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) { + pickedDefault = pickDefaultSuggestion(primaryCode); } else { commitTyped(ic); } } - if (mJustAddedAutoSpace && primaryCode == Keyboard.CODE_ENTER) { - removeTrailingSpace(); - mJustAddedAutoSpace = false; - } - sendKeyChar((char)primaryCode); - // Handle the case of ". ." -> " .." with auto-space if necessary - // before changing the TextEntryState. - if (TextEntryState.isPunctuationAfterAccepted() && primaryCode == Keyboard.CODE_PERIOD) { - reswapPeriodAndSpace(); + if (mJustAddedMagicSpace) { + if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) { + sendKeyChar((char)primaryCode); + swapSwapperAndSpace(); + } else { + if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace(); + sendKeyChar((char)primaryCode); + mJustAddedMagicSpace = false; + } + } else { + sendKeyChar((char)primaryCode); } - TextEntryState.typedCharacter((char) primaryCode, true); - if (TextEntryState.isPunctuationAfterAccepted() && primaryCode != Keyboard.CODE_ENTER) { - swapPunctuationAndSpace(); - } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { - doubleSpace(); + if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { + maybeDoubleSpace(); } + + TextEntryState.typedCharacter((char) primaryCode, true, x, y); + if (pickedDefault) { CharSequence typedWord = mWord.getTypedWord(); TextEntryState.backToAcceptedDefault(typedWord); if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { - if (ic != null) { - CorrectionInfo correctionInfo = new CorrectionInfo( - mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); - ic.commitCorrection(correctionInfo); - } + InputConnectionCompatUtils.commitCorrection( + ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); if (mCandidateView != null) mCandidateView.onAutoCorrectionInverted(mBestWord); } + } + if (Keyboard.CODE_SPACE == primaryCode) { + if (!isCursorTouchingWord()) { + mHandler.cancelUpdateSuggestions(); + mHandler.cancelUpdateOldSuggestions(); + mHandler.postUpdateBigramPredictions(); + } + } else { + // Set punctuation right away. onUpdateSelection will fire but tests whether it is + // already displayed or not, so it's okay. setPunctuationSuggestions(); } mKeyboardSwitcher.updateShiftState(); @@ -1420,45 +1395,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void handleClose() { commitTyped(getCurrentInputConnection()); - mVoiceConnector.handleClose(); + mVoiceProxy.handleClose(); requestHideSelf(0); - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); } - private void saveWordInHistory(CharSequence result) { - if (mWord.size() <= 1) { - return; - } - // Skip if result is null. It happens in some edge case. - if (TextUtils.isEmpty(result)) { - return; - } - - // Make a copy of the CharSequence, since it is/could be a mutable CharSequence - final String resultCopy = result.toString(); - TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy, - new WordComposer(mWord)); - mWordHistory.add(entry); - } - - private boolean isSuggestionsRequested() { + public boolean isSuggestionsRequested() { return mIsSettingsSuggestionStripOn && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); } - private boolean isShowingPunctuationList() { - return mSuggestPuncList == mCandidateView.getSuggestions(); + public boolean isShowingPunctuationList() { + return mSettingsValues.mSuggestPuncList == mCandidateView.getSuggestions(); } - private boolean isShowingSuggestionsStrip() { + public boolean isShowingSuggestionsStrip() { return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE && mOrientation == Configuration.ORIENTATION_PORTRAIT); } - private boolean isCandidateStripVisible() { + public boolean isCandidateStripVisible() { if (mCandidateView == null) return false; if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting()) @@ -1474,7 +1433,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (DEBUG) { Log.d(TAG, "Switch to keyboard view."); } - View v = mKeyboardSwitcher.getInputView(); + View v = mKeyboardSwitcher.getKeyboardView(); if (v != null) { // Confirms that the keyboard view doesn't have parent view. ViewParent p = v.getParent(); @@ -1483,7 +1442,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } setInputView(v); } - setCandidatesViewShown(isCandidateStripVisible()); + setSuggestionStripShown(isCandidateStripVisible()); updateInputViewShown(); mHandler.postUpdateSuggestions(); } @@ -1493,9 +1452,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void setSuggestions(SuggestedWords words) { - if (mVoiceConnector.getAndResetIsShowingHint()) { - setCandidatesView(mCandidateViewContainer); - } +// if (mVoiceProxy.getAndResetIsShowingHint()) { +// setCandidatesView(mCandidateViewContainer); +// } if (mCandidateView != null) { mCandidateView.setSuggestions(words); @@ -1509,33 +1468,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void updateSuggestions() { // Check if we have a suggestion engine attached. if ((mSuggest == null || !isSuggestionsRequested()) - && !mVoiceConnector.isVoiceInputHighlighted()) { + && !mVoiceProxy.isVoiceInputHighlighted()) { return; } - if (!mHasValidSuggestions) { + if (!mHasUncommittedTypedChars) { setPunctuationSuggestions(); return; } showSuggestions(mWord); } - private SuggestedWords.Builder getTypedSuggestions(WordComposer word) { - return mSuggest.getSuggestedWordBuilder(mKeyboardSwitcher.getInputView(), word, null); - } - - private void showCorrections(WordAlternatives alternatives) { - SuggestedWords.Builder builder = alternatives.getAlternatives(); - builder.setTypedWordValid(false).setHasMinimalSuggestion(false); - showSuggestions(builder.build(), alternatives.getOriginalWord()); - } - private void showSuggestions(WordComposer word) { // TODO: May need a better way of retrieving previous word CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), - mWordSeparators); + mSettingsValues.mWordSeparators); SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( - mKeyboardSwitcher.getInputView(), word, prevWord); + mKeyboardSwitcher.getKeyboardView(), word, prevWord); boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection(); final CharSequence typedWord = word.getTypedWord(); @@ -1556,19 +1505,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // in most cases, suggestion count is 1 when typed word's length is 1, but we do always // need to clear the previous state when the user starts typing a word (i.e. typed word's // length == 1). - if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid - || mCandidateView.isShowingAddToDictionaryHint()) { - builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(correctionAvailable); - } else { - final SuggestedWords previousSuggestions = mCandidateView.getSuggestions(); - if (previousSuggestions == mSuggestPuncList) - return; - builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); + if (typedWord != null) { + if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid + || mCandidateView.isShowingAddToDictionaryHint()) { + builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion( + correctionAvailable); + } else { + final SuggestedWords previousSuggestions = mCandidateView.getSuggestions(); + if (previousSuggestions == mSettingsValues.mSuggestPuncList) + return; + builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); + } } showSuggestions(builder.build(), typedWord); } - private void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { + public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { setSuggestions(suggestedWords); if (suggestedWords.size() > 0) { if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords, mSuggest)) { @@ -1581,30 +1533,31 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { mBestWord = null; } - setCandidatesViewShown(isCandidateStripVisible()); + setSuggestionStripShown(isCandidateStripVisible()); } - private boolean pickDefaultSuggestion() { + private boolean pickDefaultSuggestion(int separatorCode) { // Complete any pending candidate query first if (mHandler.hasPendingUpdateSuggestions()) { mHandler.cancelUpdateSuggestions(); updateSuggestions(); } if (mBestWord != null && mBestWord.length() > 0) { - TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); - mJustAccepted = true; - pickSuggestion(mBestWord); + TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord, separatorCode); + mExpectingUpdateSelection = true; + commitBestWord(mBestWord); // Add the word to the auto dictionary if it's not a known word addToAutoAndUserBigramDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); return true; - } return false; } + @Override public void pickSuggestionManually(int index, CharSequence suggestion) { SuggestedWords suggestions = mCandidateView.getSuggestions(); - mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); + mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion, + mSettingsValues.mWordSeparators); final boolean recorrecting = TextEntryState.isRecorrecting(); InputConnection ic = getCurrentInputConnection(); @@ -1629,23 +1582,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // If this is a punctuation, apply it through the normal key press - if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0)) - || isSuggestedPunctuation(suggestion.charAt(0)))) { + if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0)) + || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) { // Word separators are suggested before the user inputs something. // So, LatinImeLogger logs "" as a user's input. LatinImeLogger.logOnManualSuggestion( "", suggestion.toString(), index, suggestions.mWords); + // Find out whether the previous character is a space. If it is, as a special case + // for punctuation entered through the suggestion strip, it should be considered + // a magic space even if it was a normal space. This is meant to help in case the user + // pressed space on purpose of displaying the suggestion strip punctuation. final char primaryCode = suggestion.charAt(0); + final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : ""; + final int toLeft = (ic == null || TextUtils.isEmpty(beforeText)) + ? 0 : beforeText.charAt(0); + final boolean oldMagicSpace = mJustAddedMagicSpace; + if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true; onCodeInput(primaryCode, new int[] { primaryCode }, KeyboardActionListener.NOT_A_TOUCH_COORDINATE, KeyboardActionListener.NOT_A_TOUCH_COORDINATE); + mJustAddedMagicSpace = oldMagicSpace; if (ic != null) { ic.endBatchEdit(); } return; } - mJustAccepted = true; - pickSuggestion(suggestion); + if (!mHasUncommittedTypedChars) { + // If we are not composing a word, then it was a suggestion inferred from + // context - no user input. We should reset the word composer. + mWord.reset(); + } + mExpectingUpdateSelection = true; + commitBestWord(suggestion); // Add the word to the auto dictionary if it's not a known word if (index == 0) { addToAutoAndUserBigramDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); @@ -1656,9 +1624,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen index, suggestions.mWords); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space - if (mAutoSpace && !recorrecting) { - sendSpace(); - mJustAddedAutoSpace = true; + if (mShouldInsertMagicSpace && !recorrecting) { + sendMagicSpace(); } // We should show the hint if the user pressed the first entry AND either: @@ -1680,13 +1647,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Fool the state watcher so that a subsequent backspace will not do a revert, unless // we just did a correction, in which case we need to stay in // TextEntryState.State.PICKED_SUGGESTION state. - TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true); - setPunctuationSuggestions(); - } else if (!showingAddToDictionaryHint) { + TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); + } + if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. - clearSuggestions(); - mHandler.postUpdateOldSuggestions(); + updateBigramPredictions(); + // Updating the predictions right away may be slow and feel unresponsive on slower + // terminals. On the other hand if we just postUpdateBigramPredictions() it will + // take a noticeable delay to update them which may feel uneasy. } if (showingAddToDictionaryHint) { mCandidateView.showAddToDictionaryHint(suggestion); @@ -1699,107 +1669,50 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen /** * Commits the chosen word to the text field and saves it for later * retrieval. - * @param suggestion the suggestion picked by the user to be committed to - * the text field */ - private void pickSuggestion(CharSequence suggestion) { + private void commitBestWord(CharSequence bestWord) { KeyboardSwitcher switcher = mKeyboardSwitcher; if (!switcher.isKeyboardAvailable()) return; InputConnection ic = getCurrentInputConnection(); if (ic != null) { - mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators); - ic.commitText(suggestion, 1); + mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators); + SuggestedWords suggestedWords = mCandidateView.getSuggestions(); + ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( + this, bestWord, suggestedWords), 1); } - saveWordInHistory(suggestion); - mHasValidSuggestions = false; - mCommittedLength = suggestion.length(); + mRecorrection.saveRecorrectionSuggestion(mWord, bestWord); + mHasUncommittedTypedChars = false; + mCommittedLength = bestWord.length(); } - /** - * Tries to apply any typed alternatives for the word if we have any cached alternatives, - * otherwise tries to find new corrections and completions for the word. - * @param touching The word that the cursor is touching, with position information - * @return true if an alternative was found, false otherwise. - */ - private boolean applyTypedAlternatives(EditingUtils.SelectedWord touching) { - // 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) { - foundWord = ((TypedWordAlternatives) entry).word; - } - alternatives = entry; - break; - } - } - // If we didn't find a match, at least suggest corrections as re-corrected suggestions. - if (foundWord == null - && (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))); - } - // Found a match, show suggestions - if (foundWord != null || alternatives != null) { - if (alternatives == null) { - alternatives = new TypedWordAlternatives(touching.mWord, foundWord); - } - showCorrections(alternatives); - if (foundWord != null) { - mWord = new WordComposer(foundWord); - } else { - mWord.reset(); - } - return true; - } - return false; - } + private static final WordComposer sEmptyWordComposer = new WordComposer(); + public void updateBigramPredictions() { + if (mSuggest == null || !isSuggestionsRequested()) + return; - private void setOldSuggestions() { - mVoiceConnector.setShowingVoiceSuggestions(false); - if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) { + if (!mSettingsValues.mBigramPredictionEnabled) { + setPunctuationSuggestions(); return; } - InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; - if (!mHasValidSuggestions) { - // Extract the selected or touching text - EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic, - mLastSelectionStart, mLastSelectionEnd, mWordSeparators); - if (touching != null && touching.mWord.length() > 1) { - ic.beginBatchEdit(); - - if (!mVoiceConnector.applyVoiceAlternatives(touching) - && !applyTypedAlternatives(touching)) { - abortRecorrection(true); - } else { - TextEntryState.selectedForRecorrection(); - EditingUtils.underlineWord(ic, touching); - } + final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(), + mSettingsValues.mWordSeparators); + SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( + mKeyboardSwitcher.getKeyboardView(), sEmptyWordComposer, prevWord); - ic.endBatchEdit(); - } else { - abortRecorrection(true); - setPunctuationSuggestions(); // Show the punctuation suggestions list - } + if (builder.size() > 0) { + // Explicitly supply an empty typed word (the no-second-arg version of + // showSuggestions will retrieve the word near the cursor, we don't want that here) + showSuggestions(builder.build(), ""); } else { - abortRecorrection(true); + if (!isShowingPunctuationList()) setPunctuationSuggestions(); } } - private void setPunctuationSuggestions() { - setSuggestions(mSuggestPuncList); - setCandidatesViewShown(isCandidateStripVisible()); + public void setPunctuationSuggestions() { + setSuggestions(mSettingsValues.mSuggestPuncList); + setSuggestionStripShown(isCandidateStripVisible()); } private void addToAutoAndUserBigramDictionaries(CharSequence suggestion, int frequencyDelta) { @@ -1837,27 +1750,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } if (mUserBigramDictionary != null) { + // We don't want to register as bigrams words separated by a separator. + // For example "I will, and you too" : we don't want the pair ("will" "and") to be + // a bigram. CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), - mSentenceSeparators); + mSettingsValues.mWordSeparators); if (!TextUtils.isEmpty(prevWord)) { mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); } } } - private boolean isCursorTouchingWord() { + public boolean isCursorTouchingWord() { InputConnection ic = getCurrentInputConnection(); if (ic == null) return false; CharSequence toLeft = ic.getTextBeforeCursor(1, 0); CharSequence toRight = ic.getTextAfterCursor(1, 0); if (!TextUtils.isEmpty(toLeft) - && !isWordSeparator(toLeft.charAt(0)) - && !isSuggestedPunctuation(toLeft.charAt(0))) { + && !mSettingsValues.isWordSeparator(toLeft.charAt(0)) + && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) { return true; } if (!TextUtils.isEmpty(toRight) - && !isWordSeparator(toRight.charAt(0)) - && !isSuggestedPunctuation(toRight.charAt(0))) { + && !mSettingsValues.isWordSeparator(toRight.charAt(0)) + && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) { return true; } return false; @@ -1870,53 +1786,64 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void revertLastWord(boolean deleteChar) { final int length = mComposing.length(); - if (!mHasValidSuggestions && length > 0) { + if (!mHasUncommittedTypedChars && length > 0) { final InputConnection ic = getCurrentInputConnection(); final CharSequence punctuation = ic.getTextBeforeCursor(1, 0); if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; final CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); - if (!TextUtils.isEmpty(toTheLeft) && isWordSeparator(toTheLeft.charAt(0))) { + if (!TextUtils.isEmpty(toTheLeft) + && mSettingsValues.isWordSeparator(toTheLeft.charAt(0))) { toDelete--; } ic.deleteSurroundingText(toDelete, 0); // Re-insert punctuation only when the deleted character was word separator and the // composing text wasn't equal to the auto-corrected text. if (deleteChar - && !TextUtils.isEmpty(punctuation) && isWordSeparator(punctuation.charAt(0)) + && !TextUtils.isEmpty(punctuation) + && mSettingsValues.isWordSeparator(punctuation.charAt(0)) && !TextUtils.equals(mComposing, toTheLeft)) { ic.commitText(mComposing, 1); TextEntryState.acceptedTyped(mComposing); ic.commitText(punctuation, 1); - TextEntryState.typedCharacter(punctuation.charAt(0), true); + TextEntryState.typedCharacter(punctuation.charAt(0), true, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); // Clear composing text mComposing.setLength(0); } else { - mHasValidSuggestions = true; + mHasUncommittedTypedChars = true; ic.setComposingText(mComposing, 1); TextEntryState.backspace(); } + mHandler.cancelUpdateBigramPredictions(); mHandler.postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); } } - protected String getWordSeparators() { - return mWordSeparators; + public boolean revertDoubleSpace() { + mHandler.cancelDoubleSpacesTimer(); + final InputConnection ic = getCurrentInputConnection(); + // Here we test whether we indeed have a period and a space before us. This should not + // be needed, but it's there just in case something went wrong. + final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); + if (!". ".equals(textBeforeCursor)) + return false; + ic.beginBatchEdit(); + ic.deleteSurroundingText(2, 0); + ic.commitText(" ", 1); + ic.endBatchEdit(); + return true; } public boolean isWordSeparator(int code) { - String separators = getWordSeparators(); - return separators.contains(String.valueOf((char)code)); - } - - private boolean isSentenceSeparator(int code) { - return mSentenceSeparators.contains(String.valueOf((char)code)); + return mSettingsValues.isWordSeparator(code); } - private void sendSpace() { + private void sendMagicSpace() { sendKeyChar((char)Keyboard.CODE_SPACE); + mJustAddedMagicSpace = true; mKeyboardSwitcher.updateShiftState(); } @@ -1924,28 +1851,31 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mWord.isFirstCharCapitalized(); } - // Notify that language or mode have been changed and toggleLanguage will update KeyboaredID + // Notify that language or mode have been changed and toggleLanguage will update KeyboardID // according to new language or mode. public void onRefreshKeyboard() { - toggleLanguage(true, true); - } - - // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. - private void toggleLanguage(boolean reset, boolean next) { - if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) { - mSubtypeSwitcher.toggleLanguage(reset, next); - } // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), - mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceConnector.isVoiceButtonEnabled(), - mVoiceConnector.isVoiceButtonOnPrimary()); + mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceProxy.isVoiceButtonEnabled(), + mVoiceProxy.isVoiceButtonOnPrimary()); initSuggest(); + loadSettings(); mKeyboardSwitcher.updateShiftState(); } + // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. + private void toggleLanguage(boolean next) { + if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) { + mSubtypeSwitcher.toggleLanguage(next); + } + // The following is necessary because on API levels < 10, we don't get notified when + // subtype changes. + onRefreshKeyboard(); + } + @Override public void onSwipeDown() { - if (mConfigSwipeDownDismissKeyboardEnabled) + if (mSettingsValues.mSwipeDownDismissKeyboardEnabled) handleClose(); } @@ -1964,21 +1894,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { switcher.onOtherKeyPressed(); } - mAccessibilityUtils.onPress(primaryCode, switcher); } @Override 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(withSliding); } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { switcher.onReleaseSymbol(); } - mAccessibilityUtils.onRelease(primaryCode, switcher); } @@ -2001,7 +1928,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); } if (mAudioManager != null) { - mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); + mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); } } @@ -2009,11 +1936,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode if (mAudioManager == null) { - if (mKeyboardSwitcher.getInputView() != null) { + if (mKeyboardSwitcher.getKeyboardView() != null) { updateRingerMode(); } } - if (mSoundOn && !mSilentMode) { + if (isSoundOn()) { // FIXME: Volume and enable should come from UI settings // FIXME: These should be triggered after auto-repeat logic int sound = AudioManager.FX_KEYPRESS_STANDARD; @@ -2033,10 +1960,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void vibrate() { - if (!mVibrateOn) { + if (!mSettingsValues.mVibrateOn) { return; } - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) { inputView.performHapticFeedback( HapticFeedbackConstants.KEYBOARD_TAP, @@ -2044,28 +1971,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - public void promoteToUserDictionary(String word, int frequency) { - if (mUserDictionary.isValidWord(word)) return; - mUserDictionary.addWord(word, frequency); - } - public WordComposer getCurrentWord() { return mWord; } - public boolean getPopupOn() { - return mPopupOn; + boolean isSoundOn() { + return mSettingsValues.mSoundOn && !mSilentModeOn; } private void updateCorrectionMode() { // TODO: cleanup messy flags mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; - mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) - && !mInputTypeNoAutoCorrect && mHasDictionary; - mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled) + final boolean shouldAutoCorrect = (mSettingsValues.mAutoCorrectEnabled + || mSettingsValues.mQuickFixes) && !mInputTypeNoAutoCorrect && mHasDictionary; + mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled) ? Suggest.CORRECTION_FULL - : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); - mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled) + : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); + mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect + && mSettingsValues.mAutoCorrectEnabled) ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; if (mSuggest != null) { mSuggest.setCorrectionMode(mCorrectionMode); @@ -2074,12 +1997,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void updateAutoTextEnabled() { if (mSuggest == null) return; - mSuggest.setQuickFixesEnabled(mQuickFixes + mSuggest.setQuickFixesEnabled(mSettingsValues.mQuickFixes && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); } - private void updateSuggestionVisibility(SharedPreferences prefs) { - final Resources res = mResources; + private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) { final String suggestionVisiblityStr = prefs.getString( Settings.PREF_SHOW_SUGGESTIONS_SETTING, res.getString(R.string.prefs_suggestion_visibility_default_value)); @@ -2107,121 +2029,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen startActivity(intent); } - private void loadSettings(EditorInfo attribute) { - // Get the settings preferences - final SharedPreferences prefs = mPrefs; - 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, - mResources.getBoolean(R.bool.config_default_sound_enabled)); - - mPopupOn = isPopupEnabled(prefs); - mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); - mQuickFixes = isQuickFixesEnabled(prefs); - - mAutoCorrectEnabled = isAutoCorrectEnabled(prefs); - mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs); - loadAndSetAutoCorrectionThreshold(prefs); - - mVoiceConnector.loadSettings(attribute, prefs); - - updateCorrectionMode(); - updateAutoTextEnabled(); - updateSuggestionVisibility(prefs); - SubtypeSwitcher.getInstance().loadSettings(); - } - - /** - * Load Auto correction threshold from SharedPreferences, and modify mSuggest's threshold. - */ - private void loadAndSetAutoCorrectionThreshold(SharedPreferences sp) { - // When mSuggest is not initialized, cannnot modify mSuggest's threshold. - if (mSuggest == null) return; - // When auto correction setting is turned off, the threshold is ignored. - if (!isAutoCorrectEnabled(sp)) return; - - final String currentAutoCorrectionSetting = sp.getString( - Settings.PREF_AUTO_CORRECTION_THRESHOLD, - mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); - final String[] autoCorrectionThresholdValues = mResources.getStringArray( - R.array.auto_correction_threshold_values); - // When autoCrrectionThreshold is greater than 1.0, auto correction is virtually turned off. - double autoCorrectionThreshold = Double.MAX_VALUE; - try { - final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); - if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { - autoCorrectionThreshold = Double.parseDouble( - autoCorrectionThresholdValues[arrayIndex]); - } - } catch (NumberFormatException e) { - // Whenever the threshold settings are correct, never come here. - autoCorrectionThreshold = Double.MAX_VALUE; - Log.w(TAG, "Cannot load auto correction threshold setting." - + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting - + ", autoCorrectionThresholdValues: " - + Arrays.toString(autoCorrectionThresholdValues)); - } - // TODO: This should be refactored : - // setAutoCorrectionThreshold should be called outside of this method. - mSuggest.setAutoCorrectionThreshold(autoCorrectionThreshold); - } - - private boolean isPopupEnabled(SharedPreferences sp) { - final boolean showPopupOption = getResources().getBoolean( - R.bool.config_enable_show_popup_on_keypress_option); - if (!showPopupOption) return mResources.getBoolean(R.bool.config_default_popup_preview); - return sp.getBoolean(Settings.PREF_POPUP_ON, - mResources.getBoolean(R.bool.config_default_popup_preview)); - } - - private boolean isQuickFixesEnabled(SharedPreferences sp) { - final boolean showQuickFixesOption = mResources.getBoolean( - R.bool.config_enable_quick_fixes_option); - if (!showQuickFixesOption) { - return isAutoCorrectEnabled(sp); - } - return sp.getBoolean(Settings.PREF_QUICK_FIXES, mResources.getBoolean( - R.bool.config_default_quick_fixes)); - } - - private boolean isAutoCorrectEnabled(SharedPreferences sp) { - final String currentAutoCorrectionSetting = sp.getString( - Settings.PREF_AUTO_CORRECTION_THRESHOLD, - mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); - final String autoCorrectionOff = mResources.getString( - R.string.auto_correction_threshold_mode_index_off); - return !currentAutoCorrectionSetting.equals(autoCorrectionOff); - } - - private boolean isBigramSuggestionEnabled(SharedPreferences sp) { - final boolean showBigramSuggestionsOption = mResources.getBoolean( - R.bool.config_enable_bigram_suggestions_option); - if (!showBigramSuggestionsOption) { - return isAutoCorrectEnabled(sp); - } - return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, mResources.getBoolean( - R.bool.config_default_bigram_suggestions)); - } - - private void initSuggestPuncList() { - if (mSuggestPuncs != null || mSuggestPuncList != null) - return; - SuggestedWords.Builder builder = new SuggestedWords.Builder(); - String puncs = mResources.getString(R.string.suggested_punctuations); - if (puncs != null) { - for (int i = 0; i < puncs.length(); i++) { - builder.addWord(puncs.subSequence(i, i + 1)); - } - } - mSuggestPuncList = builder.build(); - mSuggestPuncs = puncs; - } - - private boolean isSuggestedPunctuation(int code) { - return mSuggestPuncs.contains(String.valueOf((char)code)); - } - private void showSubtypeSelectorAndSettings() { final CharSequence title = getString(R.string.english_ime_input_options); final CharSequence[] items = new CharSequence[] { @@ -2235,13 +2042,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen di.dismiss(); switch (position) { case 0: - Intent intent = new Intent( - android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + Intent intent = CompatUtils.getInputLanguageSelectionIntent( + mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.putExtra(android.provider.Settings.EXTRA_INPUT_METHOD_ID, - mInputMethodId); startActivity(intent); break; case 1: @@ -2278,7 +2082,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void showOptionsMenuInternal(CharSequence title, CharSequence[] items, DialogInterface.OnClickListener listener) { - final IBinder windowToken = mKeyboardSwitcher.getInputView().getWindowToken(); + final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken(); if (windowToken == null) return; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); @@ -2307,14 +2111,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen p.println(" mComposing=" + mComposing.toString()); p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); p.println(" mCorrectionMode=" + mCorrectionMode); - p.println(" mHasValidSuggestions=" + mHasValidSuggestions); - p.println(" mAutoCorrectOn=" + mAutoCorrectOn); - p.println(" mAutoSpace=" + mAutoSpace); + p.println(" mHasUncommittedTypedChars=" + mHasUncommittedTypedChars); + p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled); + p.println(" mShouldInsertMagicSpace=" + mShouldInsertMagicSpace); p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); p.println(" TextEntryState.state=" + TextEntryState.getState()); - p.println(" mSoundOn=" + mSoundOn); - p.println(" mVibrateOn=" + mVibrateOn); - p.println(" mPopupOn=" + mPopupOn); + p.println(" mSoundOn=" + mSettingsValues.mSoundOn); + p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn); + p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn); } // Characters per second measurement @@ -2334,9 +2138,4 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); } - - @Override - public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { - SubtypeSwitcher.getInstance().updateSubtype(subtype); - } } diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index aaecfffdd..e460471a5 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -45,10 +45,10 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang String before, String after, int position, List<CharSequence> suggestions) { } - public static void logOnAutoSuggestion(String before, String after) { + public static void logOnAutoCorrection(String before, String after, int separatorCode) { } - public static void logOnAutoSuggestionCanceled() { + public static void logOnAutoCorrectionCancelled() { } public static void logOnDelete() { @@ -57,6 +57,9 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void logOnInputChar() { } + public static void logOnInputSeparator() { + } + public static void logOnException(String metaData, Throwable e) { } diff --git a/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java new file mode 100644 index 000000000..eb740e111 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.List; +import java.util.Locale; + +class PrivateBinaryDictionaryGetter { + private PrivateBinaryDictionaryGetter() {} + public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context) { + return null; + } +} diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 341d5add0..956c51e06 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -16,17 +16,21 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.voice.VoiceIMEConnector; -import com.android.inputmethod.voice.VoiceInputLogger; +import com.android.inputmethod.compat.CompatUtils; +import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; +import com.android.inputmethod.deprecated.VoiceProxy; +import com.android.inputmethod.compat.VibratorCompatWrapper; import android.app.AlertDialog; import android.app.Dialog; import android.app.backup.BackupManager; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Bundle; -import android.os.Vibrator; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; @@ -41,6 +45,7 @@ import android.text.method.LinkMovementMethod; import android.util.Log; import android.widget.TextView; +import java.util.Arrays; import java.util.Locale; public class Settings extends PreferenceActivity @@ -51,7 +56,7 @@ public class Settings extends PreferenceActivity public static final String PREF_GENERAL_SETTINGS_KEY = "general_settings"; public static final String PREF_VIBRATE_ON = "vibrate_on"; public static final String PREF_SOUND_ON = "sound_on"; - public static final String PREF_POPUP_ON = "popup_on"; + public static final String PREF_KEY_PREVIEW_POPUP_ON = "popup_on"; public static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled"; public static final String PREF_AUTO_CAP = "auto_cap"; public static final String PREF_SETTINGS_KEY = "settings_key"; @@ -60,29 +65,243 @@ public class Settings extends PreferenceActivity public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; public static final String PREF_SUBTYPES = "subtype_settings"; - public static final String PREF_PREDICTION_SETTINGS_KEY = "prediction_settings"; + public static final String PREF_CORRECTION_SETTINGS_KEY = "correction_settings"; public static final String PREF_QUICK_FIXES = "quick_fixes"; public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; + public static final String PREF_DEBUG_SETTINGS = "debug_settings"; + + public static final String PREF_NGRAM_SETTINGS_KEY = "ngram_settings"; public static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; + public static final String PREF_BIGRAM_PREDICTIONS = "bigram_prediction"; + + public static final String PREF_MISC_SETTINGS_KEY = "misc_settings"; + + public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY = + "pref_key_preview_popup_dismiss_delay"; public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; // Dialog ids private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; + public static class Values { + // From resources: + public final boolean mSwipeDownDismissKeyboardEnabled; + public final int mDelayBeforeFadeoutLanguageOnSpacebar; + public final int mDelayUpdateSuggestions; + public final int mDelayUpdateOldSuggestions; + public final int mDelayUpdateShiftState; + public final int mDurationOfFadeoutLanguageOnSpacebar; + public final float mFinalFadeoutFactorOfLanguageOnSpacebar; + public final long mDoubleSpacesTurnIntoPeriodTimeout; + public final String mWordSeparators; + public final String mMagicSpaceStrippers; + public final String mMagicSpaceSwappers; + public final String mSuggestPuncs; + public final SuggestedWords mSuggestPuncList; + + // From preferences: + public final boolean mSoundOn; // Sound setting private to Latin IME (see mSilentModeOn) + public final boolean mVibrateOn; + public final boolean mKeyPreviewPopupOn; + public final int mKeyPreviewPopupDismissDelay; + public final boolean mAutoCap; + public final boolean mQuickFixes; + public final boolean mAutoCorrectEnabled; + public final double mAutoCorrectionThreshold; + // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary + public final boolean mBigramSuggestionEnabled; + // Prediction: use bigrams to predict the next word when there is no input for it yet + public final boolean mBigramPredictionEnabled; + + public Values(final SharedPreferences prefs, final Context context, + final String localeStr) { + final Resources res = context.getResources(); + final Locale savedLocale; + if (null != localeStr) { + final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr); + savedLocale = Utils.setSystemLocale(res, keyboardLocale); + } else { + savedLocale = null; + } + + // Get the resources + mSwipeDownDismissKeyboardEnabled = res.getBoolean( + R.bool.config_swipe_down_dismiss_keyboard_enabled); + mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( + R.integer.config_delay_before_fadeout_language_on_spacebar); + mDelayUpdateSuggestions = + res.getInteger(R.integer.config_delay_update_suggestions); + mDelayUpdateOldSuggestions = res.getInteger( + R.integer.config_delay_update_old_suggestions); + mDelayUpdateShiftState = + res.getInteger(R.integer.config_delay_update_shift_state); + mDurationOfFadeoutLanguageOnSpacebar = res.getInteger( + R.integer.config_duration_of_fadeout_language_on_spacebar); + mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger( + R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; + mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( + R.integer.config_double_spaces_turn_into_period_timeout); + mMagicSpaceStrippers = res.getString(R.string.magic_space_stripping_symbols); + mMagicSpaceSwappers = res.getString(R.string.magic_space_swapping_symbols); + String wordSeparators = mMagicSpaceStrippers + mMagicSpaceSwappers + + res.getString(R.string.magic_space_promoting_symbols); + final String notWordSeparators = res.getString(R.string.non_word_separator_symbols); + for (int i = notWordSeparators.length() - 1; i >= 0; --i) { + wordSeparators = wordSeparators.replace(notWordSeparators.substring(i, i + 1), ""); + } + mWordSeparators = wordSeparators; + mSuggestPuncs = res.getString(R.string.suggested_punctuations); + // TODO: it would be nice not to recreate this each time we change the configuration + mSuggestPuncList = createSuggestPuncList(mSuggestPuncs); + + // Get the settings preferences + final boolean hasVibrator = VibratorCompatWrapper.getInstance(context).hasVibrator(); + mVibrateOn = hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false); + mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, + res.getBoolean(R.bool.config_default_sound_enabled)); + + mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res); + mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res); + mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); + mQuickFixes = isQuickFixesEnabled(prefs, res); + + mAutoCorrectEnabled = isAutoCorrectEnabled(prefs, res); + mBigramSuggestionEnabled = mAutoCorrectEnabled + && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled); + mBigramPredictionEnabled = mBigramSuggestionEnabled + && isBigramPredictionEnabled(prefs, res); + + mAutoCorrectionThreshold = getAutoCorrectionThreshold(prefs, res); + + Utils.setSystemLocale(res, savedLocale); + } + + public boolean isSuggestedPunctuation(int code) { + return mSuggestPuncs.contains(String.valueOf((char)code)); + } + + public boolean isWordSeparator(int code) { + return mWordSeparators.contains(String.valueOf((char)code)); + } + + public boolean isMagicSpaceStripper(int code) { + return mMagicSpaceStrippers.contains(String.valueOf((char)code)); + } + + public boolean isMagicSpaceSwapper(int code) { + return mMagicSpaceSwappers.contains(String.valueOf((char)code)); + } + + // Helper methods + private static boolean isQuickFixesEnabled(SharedPreferences sp, Resources resources) { + final boolean showQuickFixesOption = resources.getBoolean( + R.bool.config_enable_quick_fixes_option); + if (!showQuickFixesOption) { + return isAutoCorrectEnabled(sp, resources); + } + return sp.getBoolean(Settings.PREF_QUICK_FIXES, resources.getBoolean( + R.bool.config_default_quick_fixes)); + } + + private static boolean isAutoCorrectEnabled(SharedPreferences sp, Resources resources) { + final String currentAutoCorrectionSetting = sp.getString( + Settings.PREF_AUTO_CORRECTION_THRESHOLD, + resources.getString(R.string.auto_correction_threshold_mode_index_modest)); + final String autoCorrectionOff = resources.getString( + R.string.auto_correction_threshold_mode_index_off); + return !currentAutoCorrectionSetting.equals(autoCorrectionOff); + } + + // Public to access from KeyboardSwitcher. Should it have access to some + // process-global instance instead? + public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) { + final boolean showPopupOption = resources.getBoolean( + R.bool.config_enable_show_popup_on_keypress_option); + if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview); + return sp.getBoolean(Settings.PREF_KEY_PREVIEW_POPUP_ON, + resources.getBoolean(R.bool.config_default_popup_preview)); + } + + // Likewise + public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp, + Resources resources) { + return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, + Integer.toString(resources.getInteger(R.integer.config_delay_after_preview)))); + } + + private static boolean isBigramSuggestionEnabled(SharedPreferences sp, Resources resources, + boolean autoCorrectEnabled) { + final boolean showBigramSuggestionsOption = resources.getBoolean( + R.bool.config_enable_bigram_suggestions_option); + if (!showBigramSuggestionsOption) { + return autoCorrectEnabled; + } + return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, resources.getBoolean( + R.bool.config_default_bigram_suggestions)); + } + + private static boolean isBigramPredictionEnabled(SharedPreferences sp, + Resources resources) { + return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean( + R.bool.config_default_bigram_prediction)); + } + + private static double getAutoCorrectionThreshold(SharedPreferences sp, + Resources resources) { + final String currentAutoCorrectionSetting = sp.getString( + Settings.PREF_AUTO_CORRECTION_THRESHOLD, + resources.getString(R.string.auto_correction_threshold_mode_index_modest)); + final String[] autoCorrectionThresholdValues = resources.getStringArray( + R.array.auto_correction_threshold_values); + // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. + double autoCorrectionThreshold = Double.MAX_VALUE; + try { + final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); + if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { + autoCorrectionThreshold = Double.parseDouble( + autoCorrectionThresholdValues[arrayIndex]); + } + } catch (NumberFormatException e) { + // Whenever the threshold settings are correct, never come here. + autoCorrectionThreshold = Double.MAX_VALUE; + Log.w(TAG, "Cannot load auto correction threshold setting." + + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting + + ", autoCorrectionThresholdValues: " + + Arrays.toString(autoCorrectionThresholdValues)); + } + return autoCorrectionThreshold; + } + + private static SuggestedWords createSuggestPuncList(final String puncs) { + SuggestedWords.Builder builder = new SuggestedWords.Builder(); + if (puncs != null) { + for (int i = 0; i < puncs.length(); i++) { + builder.addWord(puncs.subSequence(i, i + 1)); + } + } + return builder.build(); + } + } + private PreferenceScreen mInputLanguageSelection; private CheckBoxPreference mQuickFixes; private ListPreference mVoicePreference; private ListPreference mSettingsKeyPreference; private ListPreference mShowCorrectionSuggestionsPreference; private ListPreference mAutoCorrectionThreshold; + private ListPreference mKeyPreviewPopupDismissDelay; + // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary private CheckBoxPreference mBigramSuggestion; + // Prediction: use bigrams to predict the next word when there is no input for it yet + private CheckBoxPreference mBigramPrediction; + private Preference mDebugSettingsPreference; private boolean mVoiceOn; private AlertDialog mDialog; - private VoiceInputLogger mLogger; + private VoiceProxy.VoiceLoggerWrapper mVoiceLogger; private boolean mOkClicked = false; private String mVoiceModeOff; @@ -92,11 +311,14 @@ public class Settings extends PreferenceActivity R.string.auto_correction_threshold_mode_index_off); final String currentSetting = mAutoCorrectionThreshold.getValue(); mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff)); + mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff)); } @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); + final Resources res = getResources(); + addPreferencesFromResource(R.xml.prefs); mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES); mInputLanguageSelection.setOnPreferenceClickListener(this); @@ -111,69 +333,92 @@ public class Settings extends PreferenceActivity mVoiceModeOff = getString(R.string.voice_mode_off); mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) .equals(mVoiceModeOff)); - mLogger = VoiceInputLogger.getLogger(this); + mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(this); mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD); mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS); + mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS); + mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS); + if (mDebugSettingsPreference != null) { + final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN); + debugSettingsIntent.setClassName(getPackageName(), DebugSettings.class.getName()); + mDebugSettingsPreference.setIntent(debugSettingsIntent); + } + ensureConsistencyOfAutoCorrectionSettings(); final PreferenceGroup generalSettings = (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS_KEY); final PreferenceGroup textCorrectionGroup = - (PreferenceGroup) findPreference(PREF_PREDICTION_SETTINGS_KEY); + (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY); - final boolean showSettingsKeyOption = getResources().getBoolean( + final boolean showSettingsKeyOption = res.getBoolean( R.bool.config_enable_show_settings_key_option); if (!showSettingsKeyOption) { generalSettings.removePreference(mSettingsKeyPreference); } - final boolean showVoiceKeyOption = getResources().getBoolean( + final boolean showVoiceKeyOption = res.getBoolean( R.bool.config_enable_show_voice_key_option); if (!showVoiceKeyOption) { generalSettings.removePreference(mVoicePreference); } - Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); - if (vibrator == null || !vibrator.hasVibrator()) { + if (!VibratorCompatWrapper.getInstance(this).hasVibrator()) { generalSettings.removePreference(findPreference(PREF_VIBRATE_ON)); } - final boolean showSubtypeSettings = getResources().getBoolean( - R.bool.config_enable_show_subtype_settings); - if (!showSubtypeSettings) { + if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) { generalSettings.removePreference(findPreference(PREF_SUBTYPES)); } - final boolean showPopupOption = getResources().getBoolean( + final boolean showPopupOption = res.getBoolean( R.bool.config_enable_show_popup_on_keypress_option); if (!showPopupOption) { - generalSettings.removePreference(findPreference(PREF_POPUP_ON)); + generalSettings.removePreference(findPreference(PREF_KEY_PREVIEW_POPUP_ON)); } - final boolean showRecorrectionOption = getResources().getBoolean( + final boolean showRecorrectionOption = res.getBoolean( R.bool.config_enable_show_recorrection_option); if (!showRecorrectionOption) { generalSettings.removePreference(findPreference(PREF_RECORRECTION_ENABLED)); } - final boolean showQuickFixesOption = getResources().getBoolean( + final boolean showQuickFixesOption = res.getBoolean( R.bool.config_enable_quick_fixes_option); if (!showQuickFixesOption) { textCorrectionGroup.removePreference(findPreference(PREF_QUICK_FIXES)); } - final boolean showBigramSuggestionsOption = getResources().getBoolean( + final boolean showBigramSuggestionsOption = res.getBoolean( R.bool.config_enable_bigram_suggestions_option); if (!showBigramSuggestionsOption) { textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_SUGGESTIONS)); + textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_PREDICTIONS)); } - final boolean showUsabilityModeStudyOption = getResources().getBoolean( + final boolean showUsabilityModeStudyOption = res.getBoolean( R.bool.config_enable_usability_study_mode_option); if (!showUsabilityModeStudyOption) { getPreferenceScreen().removePreference(findPreference(PREF_USABILITY_STUDY_MODE)); } + + mKeyPreviewPopupDismissDelay = + (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + final String[] entries = new String[] { + res.getString(R.string.key_preview_popup_dismiss_no_delay), + res.getString(R.string.key_preview_popup_dismiss_default_delay), + }; + final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger( + R.integer.config_delay_after_preview)); + mKeyPreviewPopupDismissDelay.setEntries(entries); + mKeyPreviewPopupDismissDelay.setEntryValues( + new String[] { "0", popupDismissDelayDefaultValue }); + if (null == mKeyPreviewPopupDismissDelay.getValue()) { + mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); + } + mKeyPreviewPopupDismissDelay.setEnabled( + Settings.Values.isKeyPreviewPopupEnabled(prefs, res)); } @Override @@ -181,10 +426,10 @@ public class Settings extends PreferenceActivity super.onResume(); int autoTextSize = AutoText.getSize(getListView()); if (autoTextSize < 1) { - ((PreferenceGroup) findPreference(PREF_PREDICTION_SETTINGS_KEY)) + ((PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY)) .removePreference(mQuickFixes); } - if (!VoiceIMEConnector.VOICE_INSTALLED + if (!VoiceProxy.VOICE_INSTALLED || !SpeechRecognizer.isRecognitionAvailable(this)) { getPreferenceScreen().removePreference(mVoicePreference); } else { @@ -192,6 +437,7 @@ public class Settings extends PreferenceActivity } updateSettingsKeySummary(); updateShowCorrectionSuggestionsSummary(); + updateKeyPreviewPopupDelaySummary(); } @Override @@ -210,6 +456,12 @@ public class Settings extends PreferenceActivity .equals(mVoiceModeOff)) { showVoiceConfirmation(); } + } else if (key.equals(PREF_KEY_PREVIEW_POPUP_ON)) { + final ListPreference popupDismissDelay = + (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + if (null != popupDismissDelay) { + popupDismissDelay.setEnabled(prefs.getBoolean(PREF_KEY_PREVIEW_POPUP_ON, true)); + } } ensureConsistencyOfAutoCorrectionSettings(); mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) @@ -217,21 +469,15 @@ public class Settings extends PreferenceActivity updateVoiceModeSummary(); updateSettingsKeySummary(); updateShowCorrectionSuggestionsSummary(); + updateKeyPreviewPopupDelaySummary(); } @Override public boolean onPreferenceClick(Preference pref) { if (pref == mInputLanguageSelection) { - final String action; - if (android.os.Build.VERSION.SDK_INT - >= /* android.os.Build.VERSION_CODES.HONEYCOMB */ 11) { - // 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"; - } - startActivity(new Intent(action)); + startActivity(CompatUtils.getInputLanguageSelectionIntent( + Utils.getInputMethodId(InputMethodManagerCompatWrapper.getInstance(this), + getApplicationInfo().packageName), 0)); return true; } return false; @@ -250,6 +496,11 @@ public class Settings extends PreferenceActivity [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]); } + private void updateKeyPreviewPopupDelaySummary() { + final ListPreference lp = mKeyPreviewPopupDismissDelay; + lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]); + } + private void showVoiceConfirmation() { mOkClicked = false; showDialog(VOICE_INPUT_CONFIRM_DIALOG); @@ -277,10 +528,10 @@ public class Settings extends PreferenceActivity public void onClick(DialogInterface dialog, int whichButton) { if (whichButton == DialogInterface.BUTTON_NEGATIVE) { mVoicePreference.setValue(mVoiceModeOff); - mLogger.settingsWarningDialogCancel(); + mVoiceLogger.settingsWarningDialogCancel(); } else if (whichButton == DialogInterface.BUTTON_POSITIVE) { mOkClicked = true; - mLogger.settingsWarningDialogOk(); + mVoiceLogger.settingsWarningDialogOk(); } updateVoicePreference(); } @@ -311,7 +562,7 @@ public class Settings extends PreferenceActivity AlertDialog dialog = builder.create(); mDialog = dialog; dialog.setOnDismissListener(this); - mLogger.settingsWarningDialogShown(); + mVoiceLogger.settingsWarningDialogShown(); return dialog; default: Log.e(TAG, "unknown dialog " + id); @@ -321,7 +572,7 @@ public class Settings extends PreferenceActivity @Override public void onDismiss(DialogInterface dialog) { - mLogger.settingsWarningDialogDismissed(); + mVoiceLogger.settingsWarningDialogDismissed(); if (!mOkClicked) { // This assumes that onPreferenceClick gets called first, and this if the user // agreed after the warning, we set the mOkClicked value to true. @@ -331,10 +582,6 @@ public class Settings extends PreferenceActivity private void updateVoicePreference() { boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff); - if (isChecked) { - mLogger.voiceInputSettingEnabled(); - } else { - mLogger.voiceInputSettingDisabled(); - } + mVoiceLogger.voiceInputSettingEnabled(isChecked); } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index dc14d770a..8b51af880 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -16,11 +16,12 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; +import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; +import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.LatinKeyboard; -import com.android.inputmethod.voice.SettingsUtil; -import com.android.inputmethod.voice.VoiceIMEConnector; -import com.android.inputmethod.voice.VoiceInput; import android.content.Context; import android.content.Intent; @@ -31,12 +32,10 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.os.AsyncTask; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; import java.util.ArrayList; import java.util.Arrays; @@ -53,32 +52,37 @@ public class SubtypeSwitcher { private static final String VOICE_MODE = "voice"; private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY = "requireNetworkConnectivity"; + public static final String USE_SPACEBAR_LANGUAGE_SWITCH_KEY = "use_spacebar_language_switch"; + private final TextUtils.SimpleStringSplitter mLocaleSplitter = new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); private /* final */ LatinIME mService; - private /* final */ SharedPreferences mPrefs; - private /* final */ InputMethodManager mImm; + private /* final */ InputMethodManagerCompatWrapper mImm; private /* final */ Resources mResources; private /* final */ ConnectivityManager mConnectivityManager; private /* final */ boolean mConfigUseSpacebarLanguageSwitcher; - private final ArrayList<InputMethodSubtype> mEnabledKeyboardSubtypesOfCurrentInputMethod = - new ArrayList<InputMethodSubtype>(); + private /* final */ SharedPreferences mPrefs; + private final ArrayList<InputMethodSubtypeCompatWrapper> + mEnabledKeyboardSubtypesOfCurrentInputMethod = + new ArrayList<InputMethodSubtypeCompatWrapper>(); private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); + private final LanguageBarInfo mLanguageBarInfo = new LanguageBarInfo(); /*-----------------------------------------------------------*/ // Variants which should be changed only by reload functions. private boolean mNeedsToDisplayLanguage; private boolean mIsSystemLanguageSameAsInputLanguage; - private InputMethodInfo mShortcutInputMethodInfo; - private InputMethodSubtype mShortcutSubtype; - private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod; - private InputMethodSubtype mCurrentSubtype; + private InputMethodInfoCompatWrapper mShortcutInputMethodInfo; + private InputMethodSubtypeCompatWrapper mShortcutSubtype; + private List<InputMethodSubtypeCompatWrapper> mAllEnabledSubtypesOfCurrentInputMethod; + private InputMethodSubtypeCompatWrapper mCurrentSubtype; private Locale mSystemLocale; private Locale mInputLocale; private String mInputLocaleStr; - private VoiceInput mVoiceInput; + private String mInputMethodId; + private VoiceProxy.VoiceInputWrapper mVoiceInputWrapper; /*-----------------------------------------------------------*/ private boolean mIsNetworkConnected; @@ -88,10 +92,9 @@ public class SubtypeSwitcher { } public static void init(LatinIME service, SharedPreferences prefs) { + SubtypeLocale.init(service); sInstance.initialize(service, prefs); sInstance.updateAllParameters(); - - SubtypeLocale.init(service); } private SubtypeSwitcher() { @@ -100,9 +103,8 @@ public class SubtypeSwitcher { private void initialize(LatinIME service, SharedPreferences prefs) { mService = service; - mPrefs = prefs; mResources = service.getResources(); - mImm = (InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE); + mImm = InputMethodManagerCompatWrapper.getInstance(service); mConnectivityManager = (ConnectivityManager) service.getSystemService( Context.CONNECTIVITY_SERVICE); mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); @@ -112,15 +114,12 @@ public class SubtypeSwitcher { mInputLocaleStr = null; mCurrentSubtype = null; mAllEnabledSubtypesOfCurrentInputMethod = null; - // TODO: Voice input should be created here - mVoiceInput = null; - mConfigUseSpacebarLanguageSwitcher = mResources.getBoolean( - R.bool.config_use_spacebar_language_switcher); - if (mConfigUseSpacebarLanguageSwitcher) - initLanguageSwitcher(service); + mVoiceInputWrapper = null; + mPrefs = prefs; final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); mIsNetworkConnected = (info != null && info.isConnected()); + mInputMethodId = Utils.getInputMethodId(mImm, service.getPackageName()); } // Update all parameters stored in SubtypeSwitcher. @@ -134,11 +133,10 @@ public class SubtypeSwitcher { // Update parameters which are changed outside LatinIME. This parameters affect UI so they // should be updated every time onStartInputview. public void updateParametersOnStartInputView() { - if (mConfigUseSpacebarLanguageSwitcher) { - updateForSpacebarLanguageSwitch(); - } else { - updateEnabledSubtypes(); - } + mConfigUseSpacebarLanguageSwitcher = mPrefs.getBoolean(USE_SPACEBAR_LANGUAGE_SWITCH_KEY, + mService.getResources().getBoolean( + R.bool.config_use_spacebar_language_switcher)); + updateEnabledSubtypes(); updateShortcutIME(); } @@ -150,7 +148,7 @@ public class SubtypeSwitcher { null, true); mEnabledLanguagesOfCurrentInputMethod.clear(); mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); - for (InputMethodSubtype ims: mAllEnabledSubtypesOfCurrentInputMethod) { + for (InputMethodSubtypeCompatWrapper ims : mAllEnabledSubtypesOfCurrentInputMethod) { final String locale = ims.getLocale(); final String mode = ims.getMode(); mLocaleSplitter.setString(locale); @@ -172,6 +170,10 @@ public class SubtypeSwitcher { Log.w(TAG, "Last subtype was disabled. Update to the current one."); } updateSubtype(mImm.getCurrentInputMethodSubtype()); + } else { + // mLanguageBarInfo.update() will be called in updateSubtype so there is no need + // to call this in the if-clause above. + mLanguageBarInfo.update(); } } @@ -184,10 +186,10 @@ public class SubtypeSwitcher { + ", " + mShortcutSubtype.getMode()))); } // TODO: Update an icon for shortcut IME - Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts = + final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts = mImm.getShortcutInputMethodsAndSubtypes(); - for (InputMethodInfo imi: shortcuts.keySet()) { - List<InputMethodSubtype> subtypes = shortcuts.get(imi); + for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) { + List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi); // TODO: Returns the first found IMI for now. Should handle all shortcuts as // appropriate. mShortcutInputMethodInfo = imi; @@ -206,7 +208,7 @@ public class SubtypeSwitcher { } // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. - public void updateSubtype(InputMethodSubtype newSubtype) { + public void updateSubtype(InputMethodSubtypeCompatWrapper newSubtype) { final String newLocale; final String newMode; final String oldMode = getCurrentSubtypeMode(); @@ -243,32 +245,33 @@ public class SubtypeSwitcher { // We cancel its status when we change mode, while we reset otherwise. if (isKeyboardMode()) { if (modeChanged) { - if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { - mVoiceInput.cancel(); + if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) { + mVoiceInputWrapper.cancel(); } } if (modeChanged || languageChanged) { updateShortcutIME(); mService.onRefreshKeyboard(); } - } else if (isVoiceMode() && mVoiceInput != null) { + } else if (isVoiceMode() && mVoiceInputWrapper != null) { if (VOICE_MODE.equals(oldMode)) { - mVoiceInput.reset(); + mVoiceInputWrapper.reset(); } // If needsToShowWarningDialog is true, voice input need to show warning before // show recognition view. if (languageChanged || modeChanged - || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) { + || VoiceProxy.getInstance().needsToShowWarningDialog()) { triggerVoiceIME(); } } else { Log.w(TAG, "Unknown subtype mode: " + newMode); - if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { + if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != 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. - mVoiceInput.reset(); + mVoiceInputWrapper.reset(); } } + mLanguageBarInfo.update(); } // Update the current input locale from Locale string. @@ -277,14 +280,8 @@ public class SubtypeSwitcher { // "en_US" --> language: en & country: US // "en" --> language: en // "" --> the system locale - mLocaleSplitter.setString(inputLocaleStr); - if (mLocaleSplitter.hasNext()) { - String language = mLocaleSplitter.next(); - if (mLocaleSplitter.hasNext()) { - mInputLocale = new Locale(language, mLocaleSplitter.next()); - } else { - mInputLocale = new Locale(language); - } + if (!TextUtils.isEmpty(inputLocaleStr)) { + mInputLocale = Utils.constructLocaleFromString(inputLocaleStr); mInputLocaleStr = inputLocaleStr; } else { mInputLocale = mSystemLocale; @@ -303,25 +300,47 @@ public class SubtypeSwitcher { //////////////////////////// public void switchToShortcutIME() { - final IBinder token = mService.getWindow().getWindow().getAttributes().token; - if (token == null || mShortcutInputMethodInfo == null) { + if (mShortcutInputMethodInfo == null) { return; } + final String imiId = mShortcutInputMethodInfo.getId(); - final InputMethodSubtype subtype = mShortcutSubtype; - new Thread("SwitchToShortcutIME") { + final InputMethodSubtypeCompatWrapper subtype = mShortcutSubtype; + switchToTargetIME(imiId, subtype); + } + + private void switchToTargetIME( + final String imiId, final InputMethodSubtypeCompatWrapper subtype) { + final IBinder token = mService.getWindow().getWindow().getAttributes().token; + if (token == null) { + return; + } + new AsyncTask<Void, Void, Void>() { @Override - public void run() { + protected Void doInBackground(Void... params) { mImm.setInputMethodAndSubtype(token, imiId, subtype); + return null; } - }.start(); + + @Override + protected void onPostExecute(Void result) { + // Calls in this method need to be done in the same thread as the thread which + // called switchToShortcutIME(). + + // Notify an event that the current subtype was changed. This event will be + // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented + // when the API level is 10 or previous. + mService.notifyOnCurrentInputMethodSubtypeChanged(subtype); + } + }.execute(); } public Drawable getShortcutIcon() { return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype); } - private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) { + private Drawable getSubtypeIcon( + InputMethodInfoCompatWrapper imi, InputMethodSubtypeCompatWrapper subtype) { final PackageManager pm = mService.getPackageManager(); if (imi != null) { final String imiPackageName = imi.getPackageName(); @@ -360,11 +379,16 @@ public class SubtypeSwitcher { return false; if (mShortcutSubtype == null) return true; + // For compatibility, if the shortcut subtype is dummy, we assume the shortcut IME + // (built-in voice dummy subtype) is available. + if (!mShortcutSubtype.hasOriginalObject()) return true; final boolean allowsImplicitlySelectedSubtypes = true; - for (final InputMethodSubtype enabledSubtype : mImm.getEnabledInputMethodSubtypeList( - mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) { - if (enabledSubtype.equals(mShortcutSubtype)) + for (final InputMethodSubtypeCompatWrapper enabledSubtype : + mImm.getEnabledInputMethodSubtypeList( + mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) { + if (enabledSubtype.equals(mShortcutSubtype)) { return true; + } } return false; } @@ -389,7 +413,7 @@ public class SubtypeSwitcher { final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); final LatinKeyboard keyboard = switcher.getLatinKeyboard(); if (keyboard != null) { - keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getInputView()); + keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView()); } } @@ -398,11 +422,7 @@ public class SubtypeSwitcher { ////////////////////////////////// public int getEnabledKeyboardLocaleCount() { - if (mConfigUseSpacebarLanguageSwitcher) { - return mLanguageSwitcher.getLocaleCount(); - } else { - return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); - } + return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); } public boolean useSpacebarLanguageSwitcher() { @@ -414,90 +434,40 @@ public class SubtypeSwitcher { } public Locale getInputLocale() { - if (mConfigUseSpacebarLanguageSwitcher) { - return mLanguageSwitcher.getInputLocale(); - } else { - return mInputLocale; - } + return mInputLocale; } public String getInputLocaleStr() { - if (mConfigUseSpacebarLanguageSwitcher) { - String inputLanguage = null; - inputLanguage = mLanguageSwitcher.getInputLanguage(); - // Should return system locale if there is no Language available. - if (inputLanguage == null) { - inputLanguage = getSystemLocale().getLanguage(); - } - return inputLanguage; - } else { - return mInputLocaleStr; - } + return mInputLocaleStr; } public String[] getEnabledLanguages() { - 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[enabledLanguageCount]); + 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[enabledLanguageCount]); } public Locale getSystemLocale() { - if (mConfigUseSpacebarLanguageSwitcher) { - return mLanguageSwitcher.getSystemLocale(); - } else { - return mSystemLocale; - } + return mSystemLocale; } public boolean isSystemLanguageSameAsInputLanguage() { - if (mConfigUseSpacebarLanguageSwitcher) { - return getSystemLocale().getLanguage().equalsIgnoreCase( - getInputLocaleStr().substring(0, 2)); - } else { - return mIsSystemLanguageSameAsInputLanguage; - } + return mIsSystemLanguageSameAsInputLanguage; } public void onConfigurationChanged(Configuration conf) { final Locale systemLocale = conf.locale; // If system configuration was changed, update all parameters. if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { - if (mConfigUseSpacebarLanguageSwitcher) { - // If the system locale changes and is different from the saved - // locale (mSystemLocale), then reload the input locale list from the - // latin ime settings (shared prefs) and reset the input locale - // to the first one. - mLanguageSwitcher.loadLocales(mPrefs); - mLanguageSwitcher.setSystemLocale(systemLocale); - } else { - updateAllParameters(); - } + updateAllParameters(); } } - /** - * Change system locale for this application - * @param newLocale - * @return oldLocale - */ - public Locale changeSystemLocale(Locale newLocale) { - Configuration conf = mResources.getConfiguration(); - Locale oldLocale = conf.locale; - conf.locale = newLocale; - mResources.updateConfiguration(conf, mResources.getDisplayMetrics()); - return oldLocale; - } - public boolean isKeyboardMode() { return KEYBOARD_MODE.equals(getCurrentSubtypeMode()); } @@ -507,9 +477,9 @@ public class SubtypeSwitcher { // Voice Input functions // /////////////////////////// - public boolean setVoiceInput(VoiceInput vi) { - if (mVoiceInput == null && vi != null) { - mVoiceInput = vi; + public boolean setVoiceInputWrapper(VoiceProxy.VoiceInputWrapper vi) { + if (mVoiceInputWrapper == null && vi != null) { + mVoiceInputWrapper = vi; if (isVoiceMode()) { if (DBG) { Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr()); @@ -525,17 +495,85 @@ public class SubtypeSwitcher { return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode()); } + public boolean isDummyVoiceMode() { + return mCurrentSubtype != null && mCurrentSubtype.getOriginalObject() == null + && VOICE_MODE.equals(getCurrentSubtypeMode()); + } + private void triggerVoiceIME() { if (!mService.isInputViewShown()) return; - VoiceIMEConnector.getInstance().startListening(false, - KeyboardSwitcher.getInstance().getInputView().getWindowToken()); + VoiceProxy.getInstance().startListening(false, + KeyboardSwitcher.getInstance().getKeyboardView().getWindowToken()); } ////////////////////////////////////// // Spacebar Language Switch support // ////////////////////////////////////// - private LanguageSwitcher mLanguageSwitcher; + private class LanguageBarInfo { + private int mCurrentKeyboardSubtypeIndex; + private InputMethodSubtypeCompatWrapper mNextKeyboardSubtype; + private InputMethodSubtypeCompatWrapper mPreviousKeyboardSubtype; + private String mNextLanguage; + private String mPreviousLanguage; + public LanguageBarInfo() { + update(); + } + + private String getNextLanguage() { + return mNextLanguage; + } + + private String getPreviousLanguage() { + return mPreviousLanguage; + } + + public InputMethodSubtypeCompatWrapper getNextKeyboardSubtype() { + return mNextKeyboardSubtype; + } + + public InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtype() { + return mPreviousKeyboardSubtype; + } + + public void update() { + if (!mConfigUseSpacebarLanguageSwitcher + || mEnabledKeyboardSubtypesOfCurrentInputMethod == null + || mEnabledKeyboardSubtypesOfCurrentInputMethod.size() == 0) return; + mCurrentKeyboardSubtypeIndex = getCurrentIndex(); + mNextKeyboardSubtype = getNextKeyboardSubtypeInternal(mCurrentKeyboardSubtypeIndex); + Locale locale = Utils.constructLocaleFromString(mNextKeyboardSubtype.getLocale()); + mNextLanguage = getFullDisplayName(locale, true); + mPreviousKeyboardSubtype = getPreviousKeyboardSubtypeInternal( + mCurrentKeyboardSubtypeIndex); + locale = Utils.constructLocaleFromString(mPreviousKeyboardSubtype.getLocale()); + mPreviousLanguage = getFullDisplayName(locale, true); + } + + private int normalize(int index) { + final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); + final int ret = index % N; + return ret < 0 ? ret + N : ret; + } + + private int getCurrentIndex() { + final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); + for (int i = 0; i < N; ++i) { + if (mEnabledKeyboardSubtypesOfCurrentInputMethod.get(i).equals(mCurrentSubtype)) { + return i; + } + } + return 0; + } + + private InputMethodSubtypeCompatWrapper getNextKeyboardSubtypeInternal(int index) { + return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index + 1)); + } + + private InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtypeInternal(int index) { + return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index - 1)); + } + } public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) { if (returnsNameInThisLocale) { @@ -546,7 +584,12 @@ public class SubtypeSwitcher { } public static String getDisplayLanguage(Locale locale) { - return toTitleCase(locale.getDisplayLanguage(locale)); + return toTitleCase(SubtypeLocale.getFullDisplayName(locale)); + } + + public static String getMiddleDisplayLanguage(Locale locale) { + return toTitleCase((Utils.constructLocaleFromString( + locale.getLanguage()).getDisplayLanguage(locale))); } public static String getShortDisplayLanguage(Locale locale) { @@ -560,32 +603,16 @@ public class SubtypeSwitcher { return Character.toUpperCase(s.charAt(0)) + s.substring(1); } - private void updateForSpacebarLanguageSwitch() { - // We need to update mNeedsToDisplayLanguage in onStartInputView because - // getEnabledKeyboardLocaleCount could have been changed. - mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 - && getSystemLocale().getLanguage().equalsIgnoreCase( - getInputLocale().getLanguage())); - } - public String getInputLanguageName() { return getDisplayLanguage(getInputLocale()); } public String getNextInputLanguageName() { - if (mConfigUseSpacebarLanguageSwitcher) { - return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale()); - } else { - return ""; - } + return mLanguageBarInfo.getNextLanguage(); } public String getPreviousInputLanguageName() { - if (mConfigUseSpacebarLanguageSwitcher) { - return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale()); - } else { - return ""; - } + return mLanguageBarInfo.getPreviousLanguage(); } ///////////////////////////// @@ -612,60 +639,36 @@ public class SubtypeSwitcher { } - // 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 = - "en " + - "en_US " + - "en_GB " + - "en_AU " + - "en_CA " + - "en_IE " + - "en_IN " + - "en_NZ " + - "en_SG " + - "en_ZA "; - public boolean isVoiceSupported(String locale) { // Get the current list of supported locales and check the current locale against that // list. We cache this value so as not to check it every time the user starts a voice // input. Because this method is called by onStartInputView, this should mean that as // long as the locale doesn't change while the user is keeping the IME open, the // value should never be stale. - String supportedLocalesString = SettingsUtil.getSettingsString( - mService.getContentResolver(), - SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, - DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); + String supportedLocalesString = VoiceProxy.getSupportedLocalesString( + mService.getContentResolver()); List<String> voiceInputSupportedLocales = Arrays.asList( supportedLocalesString.split("\\s+")); return voiceInputSupportedLocales.contains(locale); } - public void loadSettings() { - if (mConfigUseSpacebarLanguageSwitcher) { - mLanguageSwitcher.loadLocales(mPrefs); - } + private void changeToNextSubtype() { + final InputMethodSubtypeCompatWrapper subtype = + mLanguageBarInfo.getNextKeyboardSubtype(); + switchToTargetIME(mInputMethodId, subtype); } - public void toggleLanguage(boolean reset, boolean next) { - if (mConfigUseSpacebarLanguageSwitcher) { - if (reset) { - mLanguageSwitcher.reset(); - } else { - if (next) { - mLanguageSwitcher.next(); - } else { - mLanguageSwitcher.prev(); - } - } - mLanguageSwitcher.persist(mPrefs); - } + private void changeToPreviousSubtype() { + final InputMethodSubtypeCompatWrapper subtype = + mLanguageBarInfo.getPreviousKeyboardSubtype(); + switchToTargetIME(mInputMethodId, subtype); } - private void initLanguageSwitcher(LatinIME service) { - final Configuration conf = service.getResources().getConfiguration(); - mLanguageSwitcher = new LanguageSwitcher(service); - mLanguageSwitcher.loadLocales(mPrefs); - mLanguageSwitcher.setSystemLocale(conf.locale); + public void toggleLanguage(boolean next) { + if (next) { + changeToNextSubtype(); + } else { + changeToPreviousSubtype(); + } } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 0de474e59..ca75866c0 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -47,7 +48,7 @@ public class Suggest implements Dictionary.WordCallback { /** * Words that appear in both bigram and unigram data gets multiplier ranging from - * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the frequency score from + * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the score from * bigram data. */ public static final double BIGRAM_MULTIPLIER_MIN = 1.2; @@ -55,7 +56,7 @@ public class Suggest implements Dictionary.WordCallback { /** * Maximum possible bigram frequency. Will depend on how many bits are being used in data - * structure. Maximum bigram freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier. + * structure. Maximum bigram frequency will get the BIGRAM_MULTIPLIER_MAX as the multiplier. */ public static final int MAXIMUM_BIGRAM_FREQUENCY = 127; @@ -74,13 +75,11 @@ public class Suggest implements Dictionary.WordCallback { public static final String DICT_KEY_USER_BIGRAM = "user_bigram"; public static final String DICT_KEY_WHITELIST ="whitelist"; - static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; - private static final boolean DBG = LatinImeLogger.sDBG; private AutoCorrection mAutoCorrection; - private BinaryDictionary mMainDict; + private Dictionary mMainDict; private WhitelistDictionary mWhiteListDictionary; private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>(); private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>(); @@ -92,13 +91,13 @@ public class Suggest implements Dictionary.WordCallback { private boolean mQuickFixesEnabled; private double mAutoCorrectionThreshold; - private int[] mPriorities = new int[mPrefMaxSuggestions]; - private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + private int[] mScores = new int[mPrefMaxSuggestions]; + private int[] mBigramScores = new int[PREF_MAX_BIGRAMS]; private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>(); private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); - private String mLowerOriginalWord; + private CharSequence mTypedWord; // TODO: Remove these member variables by passing more context to addWord() callback method private boolean mIsFirstCharCapitalized; @@ -106,15 +105,18 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; - public Suggest(Context context, int dictionaryResId) { - init(context, BinaryDictionary.initDictionary(context, dictionaryResId, DIC_MAIN)); + public Suggest(Context context, int dictionaryResId, Locale locale) { + init(context, DictionaryFactory.createDictionaryFromManager(context, locale, + dictionaryResId)); } - /* package for test */ Suggest(File dictionary, long startOffset, long length) { - init(null, BinaryDictionary.initDictionary(dictionary, startOffset, length, DIC_MAIN)); + /* package for test */ Suggest(Context context, File dictionary, long startOffset, long length, + Flag[] flagArray) { + init(null, DictionaryFactory.createDictionaryForTest(context, dictionary, startOffset, + length, flagArray)); } - private void init(Context context, BinaryDictionary mainDict) { + private void init(Context context, Dictionary mainDict) { if (mainDict != null) { mMainDict = mainDict; mUnigramDictionaries.put(DICT_KEY_MAIN, mainDict); @@ -128,6 +130,19 @@ public class Suggest implements Dictionary.WordCallback { initPool(); } + public void resetMainDict(Context context, int dictionaryResId, Locale locale) { + final Dictionary newMainDict = DictionaryFactory.createDictionaryFromManager( + context, locale, dictionaryResId); + mMainDict = newMainDict; + if (null == newMainDict) { + mUnigramDictionaries.remove(DICT_KEY_MAIN); + mBigramDictionaries.remove(DICT_KEY_MAIN); + } else { + mUnigramDictionaries.put(DICT_KEY_MAIN, newMainDict); + mBigramDictionaries.put(DICT_KEY_MAIN, newMainDict); + } + } + private void initPool() { for (int i = 0; i < mPrefMaxSuggestions; i++) { StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); @@ -148,7 +163,7 @@ public class Suggest implements Dictionary.WordCallback { } public boolean hasMainDictionary() { - return mMainDict != null && mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; + return mMainDict != null; } public Map<String, Dictionary> getUnigramDictionaries() { @@ -207,8 +222,8 @@ public class Suggest implements Dictionary.WordCallback { throw new IllegalArgumentException("maxSuggestions must be between 1 and 100"); } mPrefMaxSuggestions = maxSuggestions; - mPriorities = new int[mPrefMaxSuggestions]; - mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + mScores = new int[mPrefMaxSuggestions]; + mBigramScores = new int[PREF_MAX_BIGRAMS]; collectGarbage(mSuggestions, mPrefMaxSuggestions); while (mStringPool.size() < mPrefMaxSuggestions) { StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); @@ -248,6 +263,16 @@ public class Suggest implements Dictionary.WordCallback { return sb; } + protected void addBigramToSuggestions(CharSequence bigram) { + final int poolSize = mStringPool.size(); + final StringBuilder sb = poolSize > 0 ? + (StringBuilder) mStringPool.remove(poolSize - 1) + : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + sb.append(bigram); + mSuggestions.add(sb); + } + // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer, CharSequence prevWordForBigram) { @@ -256,25 +281,23 @@ public class Suggest implements Dictionary.WordCallback { mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); collectGarbage(mSuggestions, mPrefMaxSuggestions); - Arrays.fill(mPriorities, 0); + Arrays.fill(mScores, 0); // Save a lowercase version of the original word CharSequence typedWord = wordComposer.getTypedWord(); if (typedWord != null) { final String typedWordString = typedWord.toString(); typedWord = typedWordString; - mLowerOriginalWord = typedWordString.toLowerCase(); // Treating USER_TYPED as UNIGRAM suggestion for logging now. LatinImeLogger.onAddSuggestedWord(typedWordString, Suggest.DIC_USER_TYPED, Dictionary.DataType.UNIGRAM); - } else { - mLowerOriginalWord = ""; } + mTypedWord = typedWord; - if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM + if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM || mCorrectionMode == CORRECTION_BASIC)) { // At first character typed, search only the bigrams - Arrays.fill(mBigramPriorities, 0); + Arrays.fill(mBigramScores, 0); collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS); if (!TextUtils.isEmpty(prevWordForBigram)) { @@ -285,21 +308,26 @@ public class Suggest implements Dictionary.WordCallback { for (final Dictionary dictionary : mBigramDictionaries.values()) { dictionary.getBigrams(wordComposer, prevWordForBigram, this); } - char currentChar = wordComposer.getTypedWord().charAt(0); - char currentCharUpper = Character.toUpperCase(currentChar); - int count = 0; - int bigramSuggestionSize = mBigramSuggestions.size(); - for (int i = 0; i < bigramSuggestionSize; i++) { - if (mBigramSuggestions.get(i).charAt(0) == currentChar - || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) { - int poolSize = mStringPool.size(); - StringBuilder sb = poolSize > 0 ? - (StringBuilder) mStringPool.remove(poolSize - 1) - : new StringBuilder(getApproxMaxWordLength()); - sb.setLength(0); - sb.append(mBigramSuggestions.get(i)); - mSuggestions.add(count++, sb); - if (count > mPrefMaxSuggestions) break; + if (TextUtils.isEmpty(typedWord)) { + // Nothing entered: return all bigrams for the previous word + int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions); + for (int i = 0; i < insertCount; ++i) { + addBigramToSuggestions(mBigramSuggestions.get(i)); + } + } else { + // Word entered: return only bigrams that match the first char of the typed word + final char currentChar = typedWord.charAt(0); + final char currentCharUpper = Character.toUpperCase(currentChar); + int count = 0; + final int bigramSuggestionSize = mBigramSuggestions.size(); + for (int i = 0; i < bigramSuggestionSize; i++) { + final CharSequence bigramSuggestion = mBigramSuggestions.get(i); + final char bigramSuggestionFirstChar = bigramSuggestion.charAt(0); + if (bigramSuggestionFirstChar == currentChar + || bigramSuggestionFirstChar == currentCharUpper) { + addBigramToSuggestions(bigramSuggestion); + if (++count > mPrefMaxSuggestions) break; + } } } } @@ -346,7 +374,7 @@ public class Suggest implements Dictionary.WordCallback { mWhiteListDictionary.getWhiteListedWord(typedWordString)); mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer, - mSuggestions, mPriorities, typedWord, mAutoCorrectionThreshold, mCorrectionMode, + mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode, autoText, whitelistedWord); if (autoText != null) { @@ -364,26 +392,25 @@ public class Suggest implements Dictionary.WordCallback { if (DBG) { double normalizedScore = mAutoCorrection.getNormalizedScore(); - ArrayList<SuggestedWords.SuggestedWordInfo> frequencyInfoList = + ArrayList<SuggestedWords.SuggestedWordInfo> scoreInfoList = new ArrayList<SuggestedWords.SuggestedWordInfo>(); - frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo("+", false)); - final int priorityLength = mPriorities.length; - for (int i = 0; i < priorityLength; ++i) { + scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("+", false)); + for (int i = 0; i < mScores.length; ++i) { if (normalizedScore > 0) { - final String priorityThreshold = Integer.toString(mPriorities[i]) + " (" + - normalizedScore + ")"; - frequencyInfoList.add( - new SuggestedWords.SuggestedWordInfo(priorityThreshold, false)); + final String scoreThreshold = String.format("%d (%4.2f)", mScores[i], + normalizedScore); + scoreInfoList.add( + new SuggestedWords.SuggestedWordInfo(scoreThreshold, false)); normalizedScore = 0.0; } else { - final String priority = Integer.toString(mPriorities[i]); - frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo(priority, false)); + final String score = Integer.toString(mScores[i]); + scoreInfoList.add(new SuggestedWords.SuggestedWordInfo(score, false)); } } - for (int i = priorityLength; i < mSuggestions.size(); ++i) { - frequencyInfoList.add(new SuggestedWords.SuggestedWordInfo("--", false)); + for (int i = mScores.length; i < mSuggestions.size(); ++i) { + scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("--", false)); } - return new SuggestedWords.Builder().addWords(mSuggestions, frequencyInfoList); + return new SuggestedWords.Builder().addWords(mSuggestions, scoreInfoList); } return new SuggestedWords.Builder().addWords(mSuggestions, null); } @@ -419,52 +446,37 @@ public class Suggest implements Dictionary.WordCallback { return mAutoCorrection.hasAutoCorrection(); } - private static boolean compareCaseInsensitive(final String lowerOriginalWord, - final char[] word, final int offset, final int length) { - final int originalLength = lowerOriginalWord.length(); - if (originalLength == length && Character.isUpperCase(word[offset])) { - for (int i = 0; i < originalLength; i++) { - if (lowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) { - return false; - } - } - return true; - } - return false; - } - @Override - public boolean addWord(final char[] word, final int offset, final int length, int freq, + public boolean addWord(final char[] word, final int offset, final int length, int score, final int dicTypeId, final Dictionary.DataType dataType) { Dictionary.DataType dataTypeForLog = dataType; - ArrayList<CharSequence> suggestions; - int[] priorities; - int prefMaxSuggestions; + final ArrayList<CharSequence> suggestions; + final int[] sortedScores; + final int prefMaxSuggestions; if(dataType == Dictionary.DataType.BIGRAM) { suggestions = mBigramSuggestions; - priorities = mBigramPriorities; + sortedScores = mBigramScores; prefMaxSuggestions = PREF_MAX_BIGRAMS; } else { suggestions = mSuggestions; - priorities = mPriorities; + sortedScores = mScores; prefMaxSuggestions = mPrefMaxSuggestions; } int pos = 0; // Check if it's the same word, only caps are different - if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) { + if (Utils.equalsIgnoreCase(mTypedWord, word, offset, length)) { // TODO: remove this surrounding if clause and move this logic to // getSuggestedWordBuilder. if (suggestions.size() > 0) { - final String currentHighestWordLowerCase = - suggestions.get(0).toString().toLowerCase(); + final String currentHighestWord = suggestions.get(0).toString(); // 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]) { + if (Utils.equalsIgnoreCase(currentHighestWord, word, offset, length) + && score <= sortedScores[0]) { pos = 1; } } @@ -475,24 +487,24 @@ public class Suggest implements Dictionary.WordCallback { if(bigramSuggestion >= 0) { dataTypeForLog = Dictionary.DataType.BIGRAM; // turn freq from bigram into multiplier specified above - double multiplier = (((double) mBigramPriorities[bigramSuggestion]) + double multiplier = (((double) mBigramScores[bigramSuggestion]) / MAXIMUM_BIGRAM_FREQUENCY) * (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN) + BIGRAM_MULTIPLIER_MIN; /* Log.d(TAG,"bigram num: " + bigramSuggestion + " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString() - + " currentPriority: " + freq + " bigramPriority: " - + mBigramPriorities[bigramSuggestion] + + " currentScore: " + score + " bigramScore: " + + mBigramScores[bigramSuggestion] + " multiplier: " + multiplier); */ - freq = (int)Math.round((freq * multiplier)); + score = (int)Math.round((score * multiplier)); } } - // Check the last one's priority and bail - if (priorities[prefMaxSuggestions - 1] >= freq) return true; + // Check the last one's score and bail + if (sortedScores[prefMaxSuggestions - 1] >= score) return true; while (pos < prefMaxSuggestions) { - if (priorities[pos] < freq - || (priorities[pos] == freq && length < suggestions.get(pos).length())) { + if (sortedScores[pos] < score + || (sortedScores[pos] == score && length < suggestions.get(pos).length())) { break; } pos++; @@ -502,8 +514,8 @@ public class Suggest implements Dictionary.WordCallback { return true; } - System.arraycopy(priorities, pos, priorities, pos + 1, prefMaxSuggestions - pos - 1); - priorities[pos] = freq; + System.arraycopy(sortedScores, pos, sortedScores, pos + 1, prefMaxSuggestions - pos - 1); + sortedScores[pos] = score; int poolSize = mStringPool.size(); StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) : new StringBuilder(getApproxMaxWordLength()); diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index fe7aac7c2..a8cdfc02e 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -32,14 +32,14 @@ public class SuggestedWords { public final List<SuggestedWordInfo> mSuggestedWordInfoList; private SuggestedWords(List<CharSequence> words, boolean typedWordValid, - boolean hasMinamlSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) { + boolean hasMinimalSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) { if (words != null) { mWords = words; } else { mWords = Collections.emptyList(); } mTypedWordValid = typedWordValid; - mHasMinimalSuggestion = hasMinamlSuggestion; + mHasMinimalSuggestion = hasMinimalSuggestion; mSuggestedWordInfoList = suggestedWordInfoList; } @@ -113,8 +113,8 @@ public class SuggestedWords { return this; } - public Builder setHasMinimalSuggestion(boolean hasMinamlSuggestion) { - mHasMinimalSuggestion = hasMinamlSuggestion; + public Builder setHasMinimalSuggestion(boolean hasMinimalSuggestion) { + mHasMinimalSuggestion = hasMinimalSuggestion; return this; } diff --git a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java new file mode 100644 index 000000000..4a3f42d5d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java @@ -0,0 +1,43 @@ +/* + * 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 com.android.inputmethod.compat.SuggestionSpanUtils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class SuggestionSpanPickedNotificationReceiver extends BroadcastReceiver { + private static final boolean DBG = LatinImeLogger.sDBG; + private static final String TAG = + SuggestionSpanPickedNotificationReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if (SuggestionSpanUtils.ACTION_SUGGESTION_PICKED.equals(intent.getAction())) { + if (DBG) { + final String before = intent.getStringExtra( + SuggestionSpanUtils.SUGGESTION_SPAN_PICKED_BEFORE); + final String after = intent.getStringExtra( + SuggestionSpanUtils.SUGGESTION_SPAN_PICKED_AFTER); + Log.d(TAG, "Received notification picked: " + before + "," + after); + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index 63196430b..de13f3ae4 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.Utils.RingCharBuffer; + import android.util.Log; public class TextEntryState { @@ -43,10 +45,12 @@ public class TextEntryState { sState = newState; } - public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) { + public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord, + int separatorCode) { if (typedWord == null) return; setState(ACCEPTED_DEFAULT); - LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString()); + LatinImeLogger.logOnAutoCorrection( + typedWord.toString(), actualWord.toString(), separatorCode); if (DEBUG) displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord); } @@ -95,7 +99,7 @@ public class TextEntryState { if (DEBUG) displayState("onAbortRecorrection"); } - public static void typedCharacter(char c, boolean isSeparator) { + public static void typedCharacter(char c, boolean isSeparator, int x, int y) { final boolean isSpace = (c == ' '); switch (sState) { case IN_WORD: @@ -149,13 +153,19 @@ public class TextEntryState { setState(START); break; } + RingCharBuffer.getInstance().push(c, x, y); + if (isSeparator) { + LatinImeLogger.logOnInputSeparator(); + } else { + LatinImeLogger.logOnInputChar(); + } if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator); } public static void backspace() { if (sState == ACCEPTED_DEFAULT) { setState(UNDO_COMMIT); - LatinImeLogger.logOnAutoSuggestionCanceled(); + LatinImeLogger.logOnAutoCorrectionCancelled(); } else if (sState == UNDO_COMMIT) { setState(IN_WORD); } diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java index 656e6f8e0..5b615ca29 100644 --- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -44,12 +44,6 @@ public class UserBigramDictionary extends ExpandableDictionary { /** Maximum frequency for all pairs */ private static final int FREQUENCY_MAX = 127; - /** - * If this pair is typed 6 times, it would be suggested. - * Should be smaller than ContactsDictionary.FREQUENCY_FOR_CONTACTS_BIGRAM - */ - protected static final int SUGGEST_THRESHOLD = 6 * FREQUENCY_FOR_TYPED; - /** Maximum number of pairs. Pruning will start when databases goes above this number. */ private static int sMaxUserBigrams = 10000; @@ -164,10 +158,14 @@ public class UserBigramDictionary extends ExpandableDictionary { * Pair will be added to the userbigram database. */ public int addBigrams(String word1, String word2) { - // remove caps + // remove caps if second word is autocapitalized if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) { word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1); } + // Do not insert a word as a bigram of itself + if (word1.equals(word2)) { + return 0; + } int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED); if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX; diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index 1cbc434b6..6bdc0a857 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -16,6 +16,15 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; +import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; +import com.android.inputmethod.compat.InputTypeCompatUtils; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardId; + +import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.os.AsyncTask; @@ -26,10 +35,6 @@ 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; - -import com.android.inputmethod.keyboard.KeyboardId; import java.io.BufferedReader; import java.io.File; @@ -39,12 +44,17 @@ import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; public class Utils { private static final String TAG = Utils.class.getSimpleName(); private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4; private static boolean DBG = LatinImeLogger.sDBG; + private static boolean DBG_EDIT_DISTANCE = false; private Utils() { // Intentional empty constructor for utility class. @@ -101,17 +111,49 @@ public class Utils { } } - public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm) { - return imm.getEnabledInputMethodList().size() > 1 + public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManagerCompatWrapper imm) { + final List<InputMethodInfoCompatWrapper> enabledImis = imm.getEnabledInputMethodList(); + + // Filters out IMEs that have auxiliary subtypes only (including either implicitly or + // explicitly enabled ones). + final ArrayList<InputMethodInfoCompatWrapper> filteredImis = + new ArrayList<InputMethodInfoCompatWrapper>(); + + outerloop: + for (InputMethodInfoCompatWrapper imi : enabledImis) { + // We can return true immediately after we find two or more filtered IMEs. + if (filteredImis.size() > 1) return true; + final List<InputMethodSubtypeCompatWrapper> subtypes = + imm.getEnabledInputMethodSubtypeList(imi, true); + // IMEs that have no subtypes should be included. + if (subtypes.isEmpty()) { + filteredImis.add(imi); + continue; + } + // IMEs that have one or more non-auxiliary subtypes should be included. + for (InputMethodSubtypeCompatWrapper subtype : subtypes) { + if (!subtype.isAuxiliary()) { + filteredImis.add(imi); + continue outerloop; + } + } + } + + return filteredImis.size() > 1 // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled // input method subtype (The current IME should be LatinIME.) || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; } - public static String getInputMethodId(InputMethodManager imm, String packageName) { - for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) { + public static String getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName) { + return getInputMethodInfo(imm, packageName).getId(); + } + + public static InputMethodInfoCompatWrapper getInputMethodInfo( + InputMethodManagerCompatWrapper imm, String packageName) { + for (final InputMethodInfoCompatWrapper imi : imm.getEnabledInputMethodList()) { if (imi.getPackageName().equals(packageName)) - return imi.getId(); + return imi; } throw new RuntimeException("Can not find input method id for " + packageName); } @@ -202,11 +244,11 @@ public class Utils { return mCharBuf[mEnd]; } } - public char getLastChar() { - if (mLength < 1) { + public char getBackwardNthChar(int n) { + if (mLength <= n || n < 0) { return PLACEHOLDER_DELIMITER_CHAR; } else { - return mCharBuf[normalize(mEnd - 1)]; + return mCharBuf[normalize(mEnd - n - 1)]; } } public int getPreviousX(char c, int back) { @@ -227,9 +269,16 @@ public class Utils { return mYBuf[index]; } } - public String getLastString() { + public String getLastWord(int ignoreCharCount) { StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mLength; ++i) { + int i = ignoreCharCount; + for (; i < mLength; ++i) { + char c = mCharBuf[normalize(mEnd - 1 - i)]; + if (!((LatinIME)mContext).isWordSeparator(c)) { + break; + } + } + for (; i < mLength; ++i) { char c = mCharBuf[normalize(mEnd - 1 - i)]; if (!((LatinIME)mContext).isWordSeparator(c)) { sb.append(c); @@ -244,6 +293,8 @@ public class Utils { } } + + /* Damerau-Levenshtein distance */ public static int editDistance(CharSequence s, CharSequence t) { if (s == null || t == null) { throw new IllegalArgumentException("editDistance: Arguments should not be null."); @@ -259,12 +310,27 @@ public class Utils { } for (int i = 0; i < sl; ++i) { for (int j = 0; j < tl; ++j) { - if (Character.toLowerCase(s.charAt(i)) == Character.toLowerCase(t.charAt(j))) { - dp[i + 1][j + 1] = dp[i][j]; - } else { - dp[i + 1][j + 1] = 1 + Math.min(dp[i][j], - Math.min(dp[i + 1][j], dp[i][j + 1])); + final char sc = Character.toLowerCase(s.charAt(i)); + final char tc = Character.toLowerCase(t.charAt(j)); + final int cost = sc == tc ? 0 : 1; + dp[i + 1][j + 1] = Math.min( + dp[i][j + 1] + 1, Math.min(dp[i + 1][j] + 1, dp[i][j] + cost)); + // Overwrite for transposition cases + if (i > 0 && j > 0 + && sc == Character.toLowerCase(t.charAt(j - 1)) + && tc == Character.toLowerCase(s.charAt(i - 1))) { + dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i - 1][j - 1] + cost); + } + } + } + if (DBG_EDIT_DISTANCE) { + Log.d(TAG, "editDistance:" + s + "," + t); + for (int i = 0; i < dp.length; ++i) { + StringBuffer sb = new StringBuffer(); + for (int j = 0; j < dp[i].length; ++j) { + sb.append(dp[i][j]).append(','); } + Log.d(TAG, i + ":" + sb.toString()); } } return dp[sl][tl]; @@ -285,7 +351,7 @@ public class Utils { // In dictionary.cpp, getSuggestion() method, // suggestion scores are computed using the below formula. - // original score (called 'frequency') + // original score // := pow(mTypedLetterMultiplier (this is defined 2), // (the number of matched characters between typed word and suggested word)) // * (individual word's score which defined in the unigram dictionary, @@ -295,7 +361,7 @@ public class Utils { // (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. + // capitalization, then treat it as if the score was 255. // - If before.length() == after.length() // => multiply by mFullWordMultiplier (this is defined 2)) // So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2 @@ -306,6 +372,7 @@ public class Utils { private static final int MAX_INITIAL_SCORE = 255; private static final int TYPED_LETTER_MULTIPLIER = 2; private static final int FULL_WORD_MULTIPLIER = 2; + private static final int S_INT_MAX = 2147483647; public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) { final int beforeLength = before.length(); final int afterLength = after.length(); @@ -313,8 +380,16 @@ public class Utils { final int distance = editDistance(before, after); // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character // correction. - final double maximumScore = MAX_INITIAL_SCORE - * Math.pow(TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength)) + int spaceCount = 0; + for (int i = 0; i < afterLength; ++i) { + if (after.charAt(i) == Keyboard.CODE_SPACE) { + ++spaceCount; + } + } + if (spaceCount == afterLength) return 0; + final double maximumScore = score == S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE + * Math.pow( + TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength - spaceCount)) * FULL_WORD_MULTIPLIER; // add a weight based on edit distance. // distance <= max(afterLength, beforeLength) == afterLength, @@ -485,7 +560,7 @@ public class Utils { case InputType.TYPE_CLASS_PHONE: return KeyboardId.MODE_PHONE; case InputType.TYPE_CLASS_TEXT: - if (Utils.isEmailVariation(variation)) { + if (InputTypeCompatUtils.isEmailVariation(variation)) { return KeyboardId.MODE_EMAIL; } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { return KeyboardId.MODE_URL; @@ -501,42 +576,6 @@ public class Utils { } } - public static boolean isEmailVariation(int variation) { - return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; - } - - public static boolean isWebInputType(int inputType) { - final int variation = - inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION); - return (variation - == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT)) - || (variation - == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)) - || (variation - == (InputType.TYPE_CLASS_TEXT | 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; @@ -560,7 +599,9 @@ public class Utils { * @return main dictionary resource id */ public static int getMainDictionaryResourceId(Resources res) { - return res.getIdentifier("main", "raw", LatinIME.class.getPackage().getName()); + final String MAIN_DIC_NAME = "main"; + String packageName = LatinIME.class.getPackage().getName(); + return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName); } public static void loadNativeLibrary() { @@ -570,4 +611,108 @@ public class Utils { Log.e(TAG, "Could not load native library jni_latinime"); } } + + /** + * Returns true if a and b are equal ignoring the case of the character. + * @param a first character to check + * @param b second character to check + * @return {@code true} if a and b are equal, {@code false} otherwise. + */ + public static boolean equalsIgnoreCase(char a, char b) { + // Some language, such as Turkish, need testing both cases. + return a == b + || Character.toLowerCase(a) == Character.toLowerCase(b) + || Character.toUpperCase(a) == Character.toUpperCase(b); + } + + /** + * Returns true if a and b are equal ignoring the case of the characters, including if they are + * both null. + * @param a first CharSequence to check + * @param b second CharSequence to check + * @return {@code true} if a and b are equal, {@code false} otherwise. + */ + public static boolean equalsIgnoreCase(CharSequence a, CharSequence b) { + if (a == b) + return true; // including both a and b are null. + if (a == null || b == null) + return false; + final int length = a.length(); + if (length != b.length()) + return false; + for (int i = 0; i < length; i++) { + if (!equalsIgnoreCase(a.charAt(i), b.charAt(i))) + return false; + } + return true; + } + + /** + * Returns true if a and b are equal ignoring the case of the characters, including if a is null + * and b is zero length. + * @param a CharSequence to check + * @param b character array to check + * @param offset start offset of array b + * @param length length of characters in array b + * @return {@code true} if a and b are equal, {@code false} otherwise. + * @throws IndexOutOfBoundsException + * if {@code offset < 0 || length < 0 || offset + length > data.length}. + * @throws NullPointerException if {@code b == null}. + */ + public static boolean equalsIgnoreCase(CharSequence a, char[] b, int offset, int length) { + if (offset < 0 || length < 0 || length > b.length - offset) + throw new IndexOutOfBoundsException("array.length=" + b.length + " offset=" + offset + + " length=" + length); + if (a == null) + return length == 0; // including a is null and b is zero length. + if (a.length() != length) + return false; + for (int i = 0; i < length; i++) { + if (!equalsIgnoreCase(a.charAt(i), b[offset + i])) + return false; + } + return true; + } + + public static float getDipScale(Context context) { + final float scale = context.getResources().getDisplayMetrics().density; + return scale; + } + + /** Convert pixel to DIP */ + public static int dipToPixel(float scale, int dip) { + return (int) (dip * scale + 0.5); + } + + public static Locale setSystemLocale(Resources res, Locale newLocale) { + final Configuration conf = res.getConfiguration(); + final Locale saveLocale = conf.locale; + conf.locale = newLocale; + res.updateConfiguration(conf, res.getDisplayMetrics()); + return saveLocale; + } + + private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>(); + + public static Locale constructLocaleFromString(String localeStr) { + if (localeStr == null) + return null; + synchronized (sLocaleCache) { + if (sLocaleCache.containsKey(localeStr)) + return sLocaleCache.get(localeStr); + Locale retval = null; + String[] localeParams = localeStr.split("_", 3); + if (localeParams.length == 1) { + retval = new Locale(localeParams[0]); + } else if (localeParams.length == 2) { + retval = new Locale(localeParams[0], localeParams[1]); + } else if (localeParams.length == 3) { + retval = new Locale(localeParams[0], localeParams[1], localeParams[2]); + } + if (retval != null) { + sLocaleCache.put(localeStr, retval); + } + return retval; + } + } } diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java index 2389d4e3c..4377373d2 100644 --- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java +++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java @@ -39,6 +39,7 @@ public class WhitelistDictionary extends Dictionary { public static WhitelistDictionary init(Context context) { synchronized (sInstance) { if (context != null) { + // Wordlist is initialized by the proper language in Suggestion.java#init sInstance.initWordlist( context.getResources().getStringArray(R.array.wordlist_whitelist)); } else { diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 02583895b..af5e4b179 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -31,18 +31,18 @@ public class WordComposer { /** * The list of unicode values for each keystroke (including surrounding keys) */ - private final ArrayList<int[]> mCodes; + private ArrayList<int[]> mCodes; private int mTypedLength; - private final int[] mXCoordinates; - private final int[] mYCoordinates; + private int[] mXCoordinates; + private int[] mYCoordinates; /** * The word chosen from the candidate list, until it is committed. */ private String mPreferredWord; - private final StringBuilder mTypedWord; + private StringBuilder mTypedWord; private int mCapsCount; @@ -62,7 +62,11 @@ public class WordComposer { mYCoordinates = new int[N]; } - WordComposer(WordComposer source) { + public WordComposer(WordComposer source) { + init(source); + } + + public void init(WordComposer source) { mCodes = new ArrayList<int[]>(source.mCodes); mPreferredWord = source.mPreferredWord; mTypedWord = new StringBuilder(source.mTypedWord); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java new file mode 100644 index 000000000..e3407a269 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java @@ -0,0 +1,114 @@ +/* + * 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.spellcheck; + +import android.content.Context; +import android.content.res.Resources; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.Dictionary.DataType; +import com.android.inputmethod.latin.Dictionary.WordCallback; +import com.android.inputmethod.latin.DictionaryFactory; +import com.android.inputmethod.latin.Utils; +import com.android.inputmethod.latin.WordComposer; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +/** + * Implements spell checking methods. + */ +public class SpellChecker { + + public final Dictionary mDictionary; + + public SpellChecker(final Context context, final Locale locale) { + final Resources resources = context.getResources(); + final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); + mDictionary = DictionaryFactory.createDictionaryFromManager(context, locale, + fallbackResourceId); + } + + // Note : this must be reentrant + /** + * Finds out whether a word is in the dictionary or not. + * + * @param text the sequence containing the word to check for. + * @param start the index of the first character of the word in text. + * @param end the index of the next-to-last character in text. + * @return true if the word is in the dictionary, false otherwise. + */ + public boolean isCorrect(final CharSequence text, final int start, final int end) { + return mDictionary.isValidWord(text.subSequence(start, end)); + } + + private static class SuggestionsGatherer implements WordCallback { + private final int DEFAULT_SUGGESTION_LENGTH = 16; + private final List<String> mSuggestions = new LinkedList<String>(); + private int[] mScores = new int[DEFAULT_SUGGESTION_LENGTH]; + private int mLength = 0; + + synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score, + int dicTypeId, DataType dataType) { + if (mLength >= mScores.length) { + final int newLength = mScores.length * 2; + mScores = new int[newLength]; + } + final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score); + // binarySearch returns the index if the element exists, and -<insertion index> - 1 + // if it doesn't. See documentation for binarySearch. + final int insertionIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; + System.arraycopy(mScores, insertionIndex, mScores, insertionIndex + 1, + mLength - insertionIndex); + mLength += 1; + mScores[insertionIndex] = score; + mSuggestions.add(insertionIndex, new String(word, wordOffset, wordLength)); + return true; + } + + public List<String> getGatheredSuggestions() { + return mSuggestions; + } + } + + // Note : this must be reentrant + /** + * Gets a list of suggestions for a specific string. + * + * This returns a list of possible corrections for the text passed as an + * arguments. It may split or group words, and even perform grammatical + * analysis. + * + * @param text the sequence containing the word to check for. + * @param start the index of the first character of the word in text. + * @param end the index of the next-to-last character in text. + * @return a list of possible suggestions to replace the text. + */ + public List<String> getSuggestions(final CharSequence text, final int start, final int end) { + final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(); + final WordComposer composer = new WordComposer(); + for (int i = start; i < end; ++i) { + int character = text.charAt(i); + composer.add(character, new int[] { character }, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); + } + mDictionary.getWords(composer, suggestionsGatherer); + return suggestionsGatherer.getGatheredSuggestions(); + } +} |