diff options
Diffstat (limited to 'java/src')
25 files changed, 518 insertions, 239 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 8098dab37..143c6e848 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -234,7 +234,7 @@ public class Key implements Comparable<Key> { public Key(final Resources res, final KeyboardParams params, final KeyboardRow row, final XmlPullParser parser) throws XmlPullParserException { final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; - final int rowHeight = row.mRowHeight; + final int rowHeight = row.getRowHeight(); mHeight = rowHeight - params.mVerticalGap; final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), @@ -269,11 +269,11 @@ public class Key implements Comparable<Key> { final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview)); - mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) + mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) | row.getDefaultKeyLabelFlags(); final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId); final Locale locale = params.mId.mLocale; - int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); + int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); int moreKeysColumn = style.getInt(keyAttr, diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index f65056948..e6a674334 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -24,7 +24,7 @@ public abstract class KeyStyle { public abstract String[] getStringArray(TypedArray a, int index); public abstract String getString(TypedArray a, int index); public abstract int getInt(TypedArray a, int index, int defaultValue); - public abstract int getFlag(TypedArray a, int index); + public abstract int getFlags(TypedArray a, int index); protected KeyStyle(final KeyboardTextsSet textsSet) { mTextsSet = textsSet; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java index 6aab3e7b3..b21ea3f71 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java @@ -66,7 +66,7 @@ public final class KeyStylesSet { } @Override - public int getFlag(final TypedArray a, final int index) { + public int getFlags(final TypedArray a, final int index) { return a.getInt(index, 0); } } @@ -123,14 +123,11 @@ public final class KeyStylesSet { } @Override - public int getFlag(final TypedArray a, final int index) { - int flags = a.getInt(index, 0); - final Object value = mStyleAttributes.get(index); - if (value != null) { - flags |= (Integer)value; - } + public int getFlags(final TypedArray a, final int index) { + final Integer value = (Integer)mStyleAttributes.get(index); + final int flags = a.getInt(index, (value != null) ? value : 0); final KeyStyle parentStyle = mStyles.get(mParentStyleName); - return flags | parentStyle.getFlag(a, index); + return flags | parentStyle.getFlags(a, index); } public void readKeyAttributes(final TypedArray keyAttr) { @@ -142,13 +139,13 @@ public final class KeyStylesSet { readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel); readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); - readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags); + readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags); readString(keyAttr, R.styleable.Keyboard_Key_keyIcon); readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled); readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview); readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn); readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType); - readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); + readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); } private void readString(final TypedArray a, final int index) { @@ -163,7 +160,7 @@ public final class KeyStylesSet { } } - private void readFlag(final TypedArray a, final int index) { + private void readFlags(final TypedArray a, final int index) { if (a.hasValue(index)) { final Integer value = (Integer)mStyleAttributes.get(index); mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0)); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index 9bc52e567..3f0773e15 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -440,9 +440,6 @@ public class KeyboardBuilder<KP extends KeyboardParams> { attr, R.styleable.Keyboard_Include); final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key); int keyboardLayout = 0; - float savedDefaultKeyWidth = 0; - int savedDefaultKeyLabelFlags = 0; - int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL; try { XmlParseUtils.checkAttributeExists( keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", @@ -450,24 +447,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> { keyboardLayout = keyboardAttr.getResourceId( R.styleable.Keyboard_Include_keyboardLayout, 0); if (row != null) { - if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { - // Override current x coordinate. - row.setXPos(row.getKeyX(keyAttr)); - } - // TODO: Remove this if-clause and do the same as backgroundType below. - savedDefaultKeyWidth = row.getDefaultKeyWidth(); - if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) { - // Override default key width. - row.setDefaultKeyWidth(row.getKeyWidth(keyAttr)); - } - savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags(); - // Bitwise-or default keyLabelFlag if exists. - row.setDefaultKeyLabelFlags(keyAttr.getInt( - R.styleable.Keyboard_Key_keyLabelFlags, 0) | savedDefaultKeyLabelFlags); - savedDefaultBackgroundType = row.getDefaultBackgroundType(); - // Override default backgroundType if exists. - row.setDefaultBackgroundType(keyAttr.getInt( - R.styleable.Keyboard_Key_backgroundType, savedDefaultBackgroundType)); + // Override current x coordinate. + row.setXPos(row.getKeyX(keyAttr)); + // Push current Row attributes and update with new attributes. + row.pushRowAttributes(keyAttr); } } finally { keyboardAttr.recycle(); @@ -484,10 +467,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { parseMerge(parserForInclude, row, skip); } finally { if (row != null) { - // Restore default keyWidth, keyLabelFlags, and backgroundType. - row.setDefaultKeyWidth(savedDefaultKeyWidth); - row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags); - row.setDefaultBackgroundType(savedDefaultBackgroundType); + // Restore Row attributes. + row.popRowAttributes(); } parserForInclude.close(); } @@ -745,7 +726,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { mRightEdgeKey = null; } addEdgeSpace(mParams.mRightPadding, row); - mCurrentY += row.mRowHeight; + mCurrentY += row.getRowHeight(); mCurrentRow = null; mTopEdge = false; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java index edfcec7e1..0f9497c27 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java @@ -23,10 +23,13 @@ import android.util.Xml; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.ResourceUtils; import org.xmlpull.v1.XmlPullParser; +import java.util.ArrayDeque; + /** * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. * Some of the key size defaults can be overridden per row from what the {@link Keyboard} @@ -38,64 +41,100 @@ public final class KeyboardRow { private static final int KEYWIDTH_FILL_RIGHT = -1; private final KeyboardParams mParams; - /** Default width of a key in this row. */ - private float mDefaultKeyWidth; - /** Default height of a key in this row. */ - public final int mRowHeight; - /** Default keyLabelFlags in this row. */ - private int mDefaultKeyLabelFlags; - /** Default backgroundType for this row */ - private int mDefaultBackgroundType; + /** The height of this row. */ + private final int mRowHeight; + + private final ArrayDeque<RowAttributes> mRowAttributesStack = CollectionUtils.newArrayDeque(); + + private static class RowAttributes { + /** Default width of a key in this row. */ + public final float mDefaultKeyWidth; + /** Default keyLabelFlags in this row. */ + public final int mDefaultKeyLabelFlags; + /** Default backgroundType for this row */ + public final int mDefaultBackgroundType; + + /** + * Parse and create key attributes. This constructor is used to parse Row tag. + * + * @param keyAttr an attributes array of Row tag. + * @param defaultKeyWidth a default key width. + * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute. + */ + public RowAttributes(final TypedArray keyAttr, final float defaultKeyWidth, + final int keyboardWidth) { + mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, + keyboardWidth, keyboardWidth, defaultKeyWidth); + mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0); + mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType, + Key.BACKGROUND_TYPE_NORMAL); + } + + /** + * Parse and update key attributes using default attributes. This constructor is used + * to parse include tag. + * + * @param keyAttr an attributes array of include tag. + * @param defaultRowAttr default Row attributes. + * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute. + */ + public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr, + final int keyboardWidth) { + mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, + keyboardWidth, keyboardWidth, defaultRowAttr.mDefaultKeyWidth); + mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0) + | defaultRowAttr.mDefaultKeyLabelFlags; + mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType, + defaultRowAttr.mDefaultBackgroundType); + } + } private final int mCurrentY; // Will be updated by {@link Key}'s constructor. private float mCurrentX; - public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser, - final int y) { + public KeyboardRow(final Resources res, final KeyboardParams params, + final XmlPullParser parser, final int y) { mParams = params; final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard); mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr, - R.styleable.Keyboard_rowHeight, - params.mBaseHeight, params.mDefaultRowHeight); + R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight); keyboardAttr.recycle(); final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); - mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, - params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth); - mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType, - Key.BACKGROUND_TYPE_NORMAL); + mRowAttributesStack.push(new RowAttributes( + keyAttr, params.mDefaultKeyWidth, params.mBaseWidth)); keyAttr.recycle(); - // TODO: Initialize this with <Row> attribute as backgroundType is done. - mDefaultKeyLabelFlags = 0; mCurrentY = y; mCurrentX = 0.0f; } - public float getDefaultKeyWidth() { - return mDefaultKeyWidth; + public int getRowHeight() { + return mRowHeight; } - public void setDefaultKeyWidth(final float defaultKeyWidth) { - mDefaultKeyWidth = defaultKeyWidth; + public void pushRowAttributes(final TypedArray keyAttr) { + final RowAttributes newAttributes = new RowAttributes( + keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth); + mRowAttributesStack.push(newAttributes); } - public int getDefaultKeyLabelFlags() { - return mDefaultKeyLabelFlags; + public void popRowAttributes() { + mRowAttributesStack.pop(); } - public void setDefaultKeyLabelFlags(final int keyLabelFlags) { - mDefaultKeyLabelFlags = keyLabelFlags; + public float getDefaultKeyWidth() { + return mRowAttributesStack.peek().mDefaultKeyWidth; } - public int getDefaultBackgroundType() { - return mDefaultBackgroundType; + public int getDefaultKeyLabelFlags() { + return mRowAttributesStack.peek().mDefaultKeyLabelFlags; } - public void setDefaultBackgroundType(final int backgroundType) { - mDefaultBackgroundType = backgroundType; + public int getDefaultBackgroundType() { + return mRowAttributesStack.peek().mDefaultBackgroundType; } public void setXPos(final float keyXPos) { @@ -128,13 +167,9 @@ public final class KeyboardRow { return Math.max(keyXPos + keyboardRightEdge, mCurrentX); } - public float getKeyWidth(final TypedArray keyAttr) { - return getKeyWidth(keyAttr, mCurrentX); - } - public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) { if (keyAttr == null) { - return mDefaultKeyWidth; + return getDefaultKeyWidth(); } final int widthType = ResourceUtils.getEnumValue(keyAttr, R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); @@ -146,7 +181,7 @@ public final class KeyboardRow { return keyboardRightEdge - keyXPos; default: // KEYWIDTH_NOT_ENUM return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth, - mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth); + mParams.mBaseWidth, mParams.mBaseWidth, getDefaultKeyWidth()); } } } diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java index ebbcedc96..269b3a299 100644 --- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java +++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java @@ -42,8 +42,10 @@ abstract public class AbstractDictionaryWriter extends Dictionary { abstract public void addUnigramWord(final String word, final String shortcutTarget, final int frequency, final boolean isNotAWord); + // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve. abstract public void addBigramWords(final String word0, final String word1, - final int frequency, final boolean isValid); + final int frequency, final boolean isValid, + final long lastModifiedTime); abstract public void removeBigramWords(final String word0, final String word1); diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index c99d0e2ea..67eb7f3dd 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -70,7 +70,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private final boolean mUseFirstLastBigrams; public ContactsBinaryDictionary(final Context context, final Locale locale) { - super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS); + super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS, + false /* isUpdatable */); mLocale = locale; mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale); registerObserver(context); @@ -208,7 +209,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { false /* isNotAWord */); if (!TextUtils.isEmpty(prevWord)) { if (mUseFirstLastBigrams) { - super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM); + super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM, + 0 /* lastModifiedTime */); } } prevWord = word; diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java index 1765ce5f8..1ececd5c1 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java +++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java @@ -75,7 +75,7 @@ public class DictionaryWriter extends AbstractDictionaryWriter { @Override public void addBigramWords(final String word0, final String word1, final int frequency, - final boolean isValid) { + final boolean isValid, final long lastModifiedTime) { mFusionDictionary.setBigram(word0, word1, frequency); } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 3f11391ba..37256770a 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -20,6 +20,7 @@ import android.content.Context; import android.os.SystemClock; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.CollectionUtils; @@ -78,12 +79,18 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private final String mFilename; + /** Whether to support dynamically updating the dictionary */ + private final boolean mIsUpdatable; + /** Controls access to the shared binary dictionary file across multiple instances. */ private final DictionaryController mSharedDictionaryController; /** Controls access to the local binary dictionary for this instance. */ private final DictionaryController mLocalDictionaryController = new DictionaryController(); + /* A extension for a binary dictionary file. */ + public static final String DICT_FILE_EXTENSION = ".dict"; + /** * Abstract method for loading the unigrams and bigrams of a given dictionary in a background * thread. @@ -110,6 +117,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return controller; } + private static AbstractDictionaryWriter getDictionaryWriter(final Context context, + final String dictType, final boolean isUpdatable) { + if (isUpdatable) { + // TODO: Employ dynamically updatable DictionaryWriter. + return new DictionaryWriter(context, dictType); + } else { + return new DictionaryWriter(context, dictType); + } + } + /** * Creates a new expandable binary dictionary. * @@ -117,19 +134,22 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * @param filename The filename for this binary dictionary. Multiple dictionaries with the same * filename is supported. * @param dictType the dictionary type, as a human-readable string + * @param isUpdatable whether to support dynamically updating the dictionary. Please note that + * dynamic dictionary has negative effects on memory space and computation time. */ - public ExpandableBinaryDictionary( - final Context context, final String filename, final String dictType) { + public ExpandableBinaryDictionary(final Context context, final String filename, + final String dictType, final boolean isUpdatable) { super(dictType); mFilename = filename; mContext = context; + mIsUpdatable = isUpdatable; mBinaryDictionary = null; mSharedDictionaryController = getSharedDictionaryController(filename); - mDictionaryWriter = new DictionaryWriter(context, dictType); + mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable); } protected static String getFilenameWithLocale(final String name, final String localeStr) { - return name + "." + localeStr + ".dict"; + return name + "." + localeStr + DICT_FILE_EXTENSION; } /** @@ -137,6 +157,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ @Override public void close() { + closeBinaryDictionary(); + mLocalDictionaryController.writeLock().lock(); + try { + mDictionaryWriter.close(); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } + } + + protected void closeBinaryDictionary() { // Ensure that no other threads are accessing the local binary dictionary. mLocalDictionaryController.writeLock().lock(); try { @@ -144,7 +174,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { mBinaryDictionary.close(); mBinaryDictionary = null; } - mDictionaryWriter.close(); } finally { mLocalDictionaryController.writeLock().unlock(); } @@ -159,35 +188,70 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } /** - * Sets a word bigram in the dictionary. Used for loading a dictionary. + * Adds a word bigram in the dictionary. Used for loading a dictionary. */ - protected void setBigram(final String prevWord, final String word, final int frequency) { - mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */); + protected void addBigram(final String prevWord, final String word, final int frequency, + final long lastModifiedTime) { + mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */, + lastModifiedTime); } /** - * Dynamically adds a word unigram to the dictionary. + * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry. */ protected void addWordDynamically(final String word, final String shortcutTarget, final int frequency, final boolean isNotAWord) { - mLocalDictionaryController.writeLock().lock(); - try { - mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); - } finally { - mLocalDictionaryController.writeLock().unlock(); + if (!mIsUpdatable) { + Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename); + return; + } + // TODO: Use a queue to reflect what needs to be reflected. + if (mLocalDictionaryController.writeLock().tryLock()) { + try { + mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } } } /** - * Dynamically sets a word bigram in the dictionary. + * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry. */ - protected void setBigramDynamically(final String prevWord, final String word, - final int frequency) { - mLocalDictionaryController.writeLock().lock(); - try { - mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */); - } finally { - mLocalDictionaryController.writeLock().unlock(); + protected void addBigramDynamically(final String word0, final String word1, + final int frequency, final boolean isValid) { + if (!mIsUpdatable) { + Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: " + + mFilename); + return; + } + // TODO: Use a queue to reflect what needs to be reflected. + if (mLocalDictionaryController.writeLock().tryLock()) { + try { + mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid, + 0 /* lastTouchedTime */); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } + } + } + + /** + * Dynamically remove a word bigram in the dictionary. + */ + protected void removeBigramDynamically(final String word0, final String word1) { + if (!mIsUpdatable) { + Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: " + + mFilename); + return; + } + // TODO: Use a queue to reflect what needs to be reflected. + if (mLocalDictionaryController.writeLock().tryLock()) { + try { + mDictionaryWriter.removeBigramWords(word0, word1); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } } } @@ -277,7 +341,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { // Build the new binary dictionary final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length, - true /* useFullEditDistance */, null, mDictType, false /* isUpdatable */); + true /* useFullEditDistance */, null, mDictType, mIsUpdatable); if (mBinaryDictionary != null) { // Ensure all threads accessing the current dictionary have finished before swapping in @@ -302,9 +366,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { abstract protected boolean needsToReloadBeforeWriting(); /** - * Generates and writes a new binary dictionary based on the contents of the fusion dictionary. + * Writes a new binary dictionary based on the contents of the fusion dictionary. */ - private void generateBinaryDictionary() { + private void writeBinaryDictionary() { if (DEBUG) { Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" + mSharedDictionaryController.mLastUpdateRequestTime + " update=" @@ -367,41 +431,47 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { private final void syncReloadDictionaryInternal() { // Ensure that only one thread attempts to read or write to the shared binary dictionary // file at the same time. - mSharedDictionaryController.writeLock().lock(); + mLocalDictionaryController.writeLock().lock(); try { - final long time = SystemClock.uptimeMillis(); - final boolean dictionaryFileExists = dictionaryFileExists(); - if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) { - // If the shared dictionary file does not exist or is out of date, the first - // instance that acquires the lock will generate a new one. - if (hasContentChanged() || !dictionaryFileExists) { - // If the source content has changed or the dictionary does not exist, rebuild - // the binary dictionary. Empty dictionaries are supported (in the case where - // loadDictionaryAsync() adds nothing) in order to provide a uniform framework. + mSharedDictionaryController.writeLock().lock(); + try { + final long time = SystemClock.uptimeMillis(); + final boolean dictionaryFileExists = dictionaryFileExists(); + if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) { + // If the shared dictionary file does not exist or is out of date, the first + // instance that acquires the lock will generate a new one. + if (hasContentChanged() || !dictionaryFileExists) { + // If the source content has changed or the dictionary does not exist, + // rebuild the binary dictionary. Empty dictionaries are supported (in the + // case where loadDictionaryAsync() adds nothing) in order to provide a + // uniform framework. + mSharedDictionaryController.mLastUpdateTime = time; + writeBinaryDictionary(); + loadBinaryDictionary(); + } else { + // If not, the reload request was unnecessary so revert + // LastUpdateRequestTime to LastUpdateTime. + mSharedDictionaryController.mLastUpdateRequestTime = + mSharedDictionaryController.mLastUpdateTime; + } + } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime + < mSharedDictionaryController.mLastUpdateTime) { + // Otherwise, if the local dictionary is older than the shared dictionary, load + // the shared dictionary. + loadBinaryDictionary(); + } + if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { + // Binary dictionary is not valid. Regenerate the dictionary file. mSharedDictionaryController.mLastUpdateTime = time; - generateBinaryDictionary(); + writeBinaryDictionary(); loadBinaryDictionary(); - } else { - // If not, the reload request was unnecessary so revert LastUpdateRequestTime - // to LastUpdateTime. - mSharedDictionaryController.mLastUpdateRequestTime = - mSharedDictionaryController.mLastUpdateTime; } - } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime - < mSharedDictionaryController.mLastUpdateTime) { - // Otherwise, if the local dictionary is older than the shared dictionary, load the - // shared dictionary. - loadBinaryDictionary(); - } - if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { - // Binary dictionary is not valid. Regenerate the dictionary file. - mSharedDictionaryController.mLastUpdateTime = time; - generateBinaryDictionary(); - loadBinaryDictionary(); + mLocalDictionaryController.mLastUpdateTime = time; + } finally { + mSharedDictionaryController.writeLock().unlock(); } - mLocalDictionaryController.mLastUpdateTime = time; } finally { - mSharedDictionaryController.writeLock().unlock(); + mLocalDictionaryController.writeLock().unlock(); } } @@ -434,4 +504,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return (mLastUpdateRequestTime > mLastUpdateTime); } } + + /** + * Dynamically adds a word unigram to the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void addWordDynamicallyForTests(final String word, final String shortcutTarget, + final int frequency, final boolean isNotAWord) { + mLocalDictionaryController.writeLock().lock(); + try { + addWordDynamically(word, shortcutTarget, frequency, isNotAWord); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } + } + + /** + * Dynamically adds a word bigram in the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void addBigramDynamicallyForTests(final String word0, final String word1, + final int frequency, final boolean isValid) { + mLocalDictionaryController.writeLock().lock(); + try { + addBigramDynamically(word0, word1, frequency, isValid); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } + } + + /** + * Dynamically remove a word bigram in the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void removeBigramDynamicallyForTests(final String word0, final String word1) { + mLocalDictionaryController.writeLock().lock(); + try { + removeBigramDynamically(word0, word1); + } finally { + mLocalDictionaryController.writeLock().unlock(); + } + } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index ffe317161..ee7478ca2 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1948,7 +1948,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { + if (currentSettings.isSuggestionsRequested(mDisplayOrientation) + && currentSettings.mCurrentLanguageHasSpaces) { restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); } // We just removed a character. We need to update the auto-caps state. @@ -1977,6 +1978,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void handleCharacter(final int primaryCode, final int x, final int y, final int spaceState) { + // TODO: refactor this method to stop flipping isComposingWord around all the time, and + // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter + // which has the same name as other handle* methods but is not the same. boolean isComposingWord = mWordComposer.isComposingWord(); // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. @@ -1996,12 +2000,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen resetEntireInputState(mLastSelectionStart); isComposingWord = false; } - // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several - // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI - // thread here. - if (!isComposingWord && currentSettings.isWordCodePoint(primaryCode) + // We want to find out whether to start composing a new word with this character. If so, + // we need to reset the composing state and switch isComposingWord. The order of the + // tests is important for good performance. + // We only start composing if we're not already composing. + if (!isComposingWord + // We only start composing if this is a word code point. Essentially that means it's a + // a letter or a word connector. + && currentSettings.isWordCodePoint(primaryCode) + // We never go into composing state if suggestions are not requested. && currentSettings.isSuggestionsRequested(mDisplayOrientation) && - !mConnection.isCursorTouchingWord(currentSettings)) { + // In languages with spaces, we only start composing a word when we are not already + // touching a word. In languages without spaces, the above conditions are sufficient. + (!mConnection.isCursorTouchingWord(currentSettings) + || !currentSettings.mCurrentLanguageHasSpaces)) { // Reset entirely the composing state anyway, then start composing a new word unless // the character is a single quote. The idea here is, single quote is not a // separator and it should be treated as a normal character, except in the first @@ -2089,16 +2101,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { boolean didAutoCorrect = false; + final SettingsValues currentSettings = mSettings.getCurrent(); + // We avoid sending spaces in languages without spaces if we were composing. + final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode + && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord(); if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the separator at the current cursor position. resetEntireInputState(mLastSelectionStart); } - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mWordComposer.isComposingWord()) { + if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing if (currentSettings.mCorrectionEnabled) { - // TODO: maybe cache Strings in an <String> sparse array or something - commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); + final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR + : new String(new int[] { primaryCode }, 0, 1); + commitCurrentAutoCorrection(separator); didAutoCorrect = true; } else { commitTyped(new String(new int[]{primaryCode}, 0, 1)); @@ -2115,7 +2131,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } - sendKeyCodePoint(primaryCode); + + if (!shouldAvoidSendingCode) { + sendKeyCodePoint(primaryCode); + } if (Constants.CODE_SPACE == primaryCode) { if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { @@ -2260,11 +2279,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Get the word on which we should search the bigrams. If we are composing a word, it's // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we // should just skip whitespace if any, so 1. - // TODO: this is slow (2-way IPC) - we should probably cache this instead. final SettingsValues currentSettings = mSettings.getCurrent(); - final String prevWord = - mConnection.getNthPreviousWord(currentSettings.mWordSeparators, - mWordComposer.isComposingWord() ? 2 : 1); + final String prevWord; + if (currentSettings.mCurrentLanguageHasSpaces) { + // If we are typing in a language with spaces we can just look up the previous + // word from textview. + prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, + mWordComposer.isComposingWord() ? 2 : 1); + } else { + prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null + : mLastComposedWord.mCommittedWord; + } return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, sessionId); @@ -2534,6 +2559,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // recorrection. This is a temporary, stopgap measure that will be removed later. // TODO: remove this. if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; + // Recorrection is not supported in languages without spaces because we don't know + // how to segment them yet. + if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; // If the cursor is not touching a word, or if there is a selection, return right away. if (mLastSelectionStart != mLastSelectionEnd) return; // If we don't know the cursor location, return. @@ -2656,7 +2684,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord); } - mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); + final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { + // For languages with spaces, we revert to the typed string, but the cursor is still + // after the separator so we don't resume suggestions. If the user wants to correct + // the word, they have to press backspace again. + mConnection.commitText(stringToCommit, 1); + } else { + // For languages without spaces, we revert the typed string but the cursor is flush + // with the typed word, so we need to resume suggestions right away. + mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard()); + mConnection.setComposingText(stringToCommit, 1); + } if (mSettings.isInternal()) { LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); @@ -2674,7 +2713,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // This essentially inserts a space, and that's it. public void promotePhantomSpace() { - if (mSettings.getCurrent().shouldInsertSpacesAutomatically() + final SettingsValues currentSettings = mSettings.getCurrent(); + if (currentSettings.shouldInsertSpacesAutomatically() + && currentSettings.mCurrentLanguageHasSpaces && !mConnection.textBeforeCursorLooksLikeURL()) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_promotePhantomSpace(); @@ -2887,6 +2928,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mSuggest.hasMainDictionary(); } + // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. + @UsedForTesting + /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { + mSuggest.resetMainDict(this, locale, null); + } + public void debugDumpStateAndCrashWithException(final String context) { final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index ed6fefae4..b2bb61596 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -75,7 +75,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { public UserBinaryDictionary(final Context context, final String locale, final boolean alsoUseMoreRestrictiveLocales) { - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER); + super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER, + false /* isUpdatable */); if (null == locale) throw new NullPointerException(); // Catch the error earlier if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) { // If we don't have a locale, insert into the "all locales" user dictionary. diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java index 046c5b5dc..5e3d6d22d 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java @@ -22,6 +22,7 @@ import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup; import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; +import com.android.inputmethod.latin.makedict.decoder.HeaderReaderInterface; import com.android.inputmethod.latin.utils.JniUtils; import java.io.ByteArrayOutputStream; @@ -250,7 +251,7 @@ public final class BinaryDictDecoder { /** * Reads a string from a buffer. This is the converse of the above method. */ - private static String readString(final FusionDictionaryBufferInterface buffer) { + static String readString(final FusionDictionaryBufferInterface buffer) { final StringBuilder s = new StringBuilder(); int character = readChar(buffer); while (character != FormatSpec.INVALID_CHARACTER) { @@ -457,16 +458,13 @@ public final class BinaryDictDecoder { return result; } - // TODO: static!? This will behave erratically when used in multi-threaded code. - // We need to fix this - private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; @SuppressWarnings("unused") private static WeightedString getWordAtAddressWithParentAddress( final FusionDictionaryBufferInterface buffer, final int headerSize, final int address, final FormatOptions options) { int currentAddress = address; - int index = FormatSpec.MAX_WORD_LENGTH - 1; int frequency = Integer.MIN_VALUE; + final StringBuilder builder = new StringBuilder(); // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) { CharGroupInfo currentInfo; @@ -482,17 +480,12 @@ public final class BinaryDictDecoder { } } while (BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags, options)); if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency; - for (int i = 0; i < currentInfo.mCharacters.length; ++i) { - sGetWordBuffer[index--] = - currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1]; - } + builder.insert(0, + new String(currentInfo.mCharacters, 0, currentInfo.mCharacters.length)); if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break; currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress; } - - return new WeightedString( - new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1), - frequency); + return new WeightedString(builder.toString(), frequency); } private static WeightedString getWordAtAddressWithoutParentAddress( @@ -637,7 +630,7 @@ public final class BinaryDictDecoder { * @throws UnsupportedFormatException * @throws IOException */ - private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer) + static int checkFormatVersion(final FusionDictionaryBufferInterface buffer) throws IOException, UnsupportedFormatException { final int version = getFormatVersion(buffer); if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION @@ -651,25 +644,22 @@ public final class BinaryDictDecoder { /** * Reads a header from a buffer. - * @param buffer the buffer to read. + * @param headerReader the header reader * @throws IOException * @throws UnsupportedFormatException */ - public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer) + public static FileHeader readHeader(final HeaderReaderInterface headerReader) throws IOException, UnsupportedFormatException { - final int version = checkFormatVersion(buffer); - final int optionsFlags = buffer.readUnsignedShort(); + final int version = headerReader.readVersion(); + final int optionsFlags = headerReader.readOptionFlags(); - final HashMap<String, String> attributes = new HashMap<String, String>(); - final int headerSize; - headerSize = buffer.readInt(); + final int headerSize = headerReader.readHeaderSize(); if (headerSize < 0) { throw new UnsupportedFormatException("header size can't be negative."); } - populateOptions(buffer, headerSize, attributes); - buffer.position(headerSize); + final HashMap<String, String> attributes = headerReader.readAttributes(headerSize); final FileHeader header = new FileHeader(headerSize, new FusionDictionary.DictionaryOptions(attributes, @@ -719,14 +709,14 @@ public final class BinaryDictDecoder { } // Read header - final FileHeader header = readHeader(reader.getBuffer()); + final FileHeader fileHeader = readHeader(reader); Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>(); Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>(); - final PtNodeArray root = readNodeArray(reader.getBuffer(), header.mHeaderSize, - reverseNodeArrayMapping, reverseGroupMapping, header.mFormatOptions); + final PtNodeArray root = readNodeArray(reader.getBuffer(), fileHeader.mHeaderSize, + reverseNodeArrayMapping, reverseGroupMapping, fileHeader.mFormatOptions); - FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions); + FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions); if (null != dict) { for (final Word w : dict) { if (w.mIsBlacklistEntry) { diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java index 476d51b8e..e5735aaca 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -24,13 +24,13 @@ import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup; import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; +import com.android.inputmethod.latin.utils.ByteArrayWrapper; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; -import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; @@ -141,20 +141,20 @@ public final class BinaryDictIOUtils { * Reads unigrams and bigrams from the binary file. * Doesn't store a full memory representation of the dictionary. * - * @param reader the reader. + * @param dictReader the dict reader. * @param words the map to store the address as a key and the word as a value. * @param frequencies the map to store the address as a key and the frequency as a value. * @param bigrams the map to store the address as a key and the list of address as a value. * @throws IOException if the file can't be read. * @throws UnsupportedFormatException if the format of the file is not recognized. */ - public static void readUnigramsAndBigramsBinary(final BinaryDictReader reader, + public static void readUnigramsAndBigramsBinary(final BinaryDictReader dictReader, final Map<Integer, String> words, final Map<Integer, Integer> frequencies, final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, UnsupportedFormatException { // Read header - final FileHeader header = BinaryDictDecoder.readHeader(reader.getBuffer()); - readUnigramsAndBigramsBinaryInner(reader.getBuffer(), header.mHeaderSize, words, + final FileHeader header = BinaryDictDecoder.readHeader(dictReader); + readUnigramsAndBigramsBinaryInner(dictReader.getBuffer(), header.mHeaderSize, words, frequencies, bigrams, header.mFormatOptions); } @@ -162,19 +162,20 @@ public final class BinaryDictIOUtils { * Gets the address of the last CharGroup of the exact matching word in the dictionary. * If no match is found, returns NOT_VALID_WORD. * - * @param buffer the buffer to read. + * @param dictReader the dict reader. * @param word the word we search for. * @return the address of the terminal node. * @throws IOException if the file can't be read. * @throws UnsupportedFormatException if the format of the file is not recognized. */ @UsedForTesting - public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer, + public static int getTerminalPosition(final BinaryDictReader dictReader, final String word) throws IOException, UnsupportedFormatException { + final FusionDictionaryBufferInterface buffer = dictReader.getBuffer(); if (word == null) return FormatSpec.NOT_VALID_WORD; if (buffer.position() != 0) buffer.position(0); - final FileHeader header = BinaryDictDecoder.readHeader(buffer); + final FileHeader header = BinaryDictDecoder.readHeader(dictReader); int wordPos = 0; final int wordLen = word.codePointCount(0, word.length()); for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { @@ -507,21 +508,22 @@ public final class BinaryDictIOUtils { } /** - * Find a word from the buffer. + * Find a word using the BinaryDictReader. * - * @param buffer the buffer representing the body of the dictionary file. + * @param dictReader the dict reader * @param word the word searched * @return the found group * @throws IOException * @throws UnsupportedFormatException */ @UsedForTesting - public static CharGroupInfo findWordFromBuffer(final FusionDictionaryBufferInterface buffer, + public static CharGroupInfo findWordByBinaryDictReader(final BinaryDictReader dictReader, final String word) throws IOException, UnsupportedFormatException { - int position = getTerminalPosition(buffer, word); + int position = getTerminalPosition(dictReader, word); + final FusionDictionaryBufferInterface buffer = dictReader.getBuffer(); if (position != FormatSpec.NOT_VALID_WORD) { buffer.position(0); - final FileHeader header = BinaryDictDecoder.readHeader(buffer); + final FileHeader header = BinaryDictDecoder.readHeader(dictReader); buffer.position(position); return BinaryDictDecoder.readCharGroup(buffer, position, header.mFormatOptions); } @@ -542,16 +544,21 @@ public final class BinaryDictIOUtils { final File file, final long offset, final long length) throws FileNotFoundException, IOException, UnsupportedFormatException { final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE]; - final FileInputStream inStream = new FileInputStream(file); - try { - inStream.read(buffer); - final BinaryDictDecoder.ByteBufferWrapper wrapper = - new BinaryDictDecoder.ByteBufferWrapper(inStream.getChannel().map( - FileChannel.MapMode.READ_ONLY, offset, length)); - return BinaryDictDecoder.readHeader(wrapper); - } finally { - inStream.close(); - } + final BinaryDictReader dictReader = new BinaryDictReader(file); + dictReader.openBuffer(new BinaryDictReader.FusionDictionaryBufferFactory() { + @Override + public FusionDictionaryBufferInterface getFusionDictionaryBuffer(File file) + throws FileNotFoundException, IOException { + final FileInputStream inStream = new FileInputStream(file); + try { + inStream.read(buffer); + return new ByteArrayWrapper(buffer); + } finally { + inStream.close(); + } + } + }); + return BinaryDictDecoder.readHeader(dictReader); } public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset, diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java index f2f3c465d..6d3b31a28 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java @@ -17,7 +17,9 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.makedict.BinaryDictDecoder.CharEncoding; import com.android.inputmethod.latin.makedict.BinaryDictDecoder.FusionDictionaryBufferInterface; +import com.android.inputmethod.latin.makedict.decoder.HeaderReaderInterface; import com.android.inputmethod.latin.utils.ByteArrayWrapper; import java.io.File; @@ -27,8 +29,9 @@ import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.util.HashMap; -public class BinaryDictReader { +public class BinaryDictReader implements HeaderReaderInterface { public interface FusionDictionaryBufferFactory { public FusionDictionaryBufferInterface getFusionDictionaryBuffer(final File file) @@ -133,4 +136,34 @@ public class BinaryDictReader { openBuffer(factory); return getBuffer(); } + + // The implementation of HeaderReaderInterface + @Override + public int readVersion() throws IOException, UnsupportedFormatException { + return BinaryDictDecoder.checkFormatVersion(mFusionDictionaryBuffer); + } + + @Override + public int readOptionFlags() { + return mFusionDictionaryBuffer.readUnsignedShort(); + } + + @Override + public int readHeaderSize() { + return mFusionDictionaryBuffer.readInt(); + } + + @Override + public HashMap<String, String> readAttributes(final int headerSize) { + final HashMap<String, String> attributes = new HashMap<String, String>(); + while (mFusionDictionaryBuffer.position() < headerSize) { + // We can avoid infinite loop here since mFusionDictonary.position() is always increased + // by calling CharEncoding.readString. + final String key = CharEncoding.readString(mFusionDictionaryBuffer); + final String value = CharEncoding.readString(mFusionDictionaryBuffer); + attributes.put(key, value); + } + mFusionDictionaryBuffer.position(headerSize); + return attributes; + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java index 5d116d79c..584b793fb 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java @@ -49,17 +49,18 @@ public final class DynamicBinaryDictIOUtils { /** * Delete the word from the binary file. * - * @param buffer the buffer to write. + * @param dictReader the dict reader. * @param word the word we delete * @throws IOException * @throws UnsupportedFormatException */ @UsedForTesting - public static void deleteWord(final FusionDictionaryBufferInterface buffer, - final String word) throws IOException, UnsupportedFormatException { + public static void deleteWord(final BinaryDictReader dictReader, final String word) + throws IOException, UnsupportedFormatException { + final FusionDictionaryBufferInterface buffer = dictReader.getBuffer(); buffer.position(0); - final FileHeader header = BinaryDictDecoder.readHeader(buffer); - final int wordPosition = BinaryDictIOUtils.getTerminalPosition(buffer, word); + final FileHeader header = BinaryDictDecoder.readHeader(dictReader); + final int wordPosition = BinaryDictIOUtils.getTerminalPosition(dictReader, word); if (wordPosition == FormatSpec.NOT_VALID_WORD) return; buffer.position(wordPosition); @@ -235,7 +236,7 @@ public final class DynamicBinaryDictIOUtils { /** * Insert a word into a binary dictionary. * - * @param buffer the buffer containing the existing dictionary. + * @param dictReader the dict reader. * @param destination a stream to the underlying file, with the pointer at the end of the file. * @param word the word to insert. * @param frequency the frequency of the new word. @@ -248,16 +249,16 @@ public final class DynamicBinaryDictIOUtils { // TODO: Support batch insertion. // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary. @UsedForTesting - public static void insertWord(final FusionDictionaryBufferInterface buffer, - final OutputStream destination, final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, + public static void insertWord(final BinaryDictReader dictReader, final OutputStream destination, + final String word, final int frequency, final ArrayList<WeightedString> bigramStrings, final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, final boolean isBlackListEntry) throws IOException, UnsupportedFormatException { final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>(); + final FusionDictionaryBufferInterface buffer = dictReader.getBuffer(); if (bigramStrings != null) { for (final WeightedString bigram : bigramStrings) { - int position = BinaryDictIOUtils.getTerminalPosition(buffer, bigram.mWord); + int position = BinaryDictIOUtils.getTerminalPosition(dictReader, bigram.mWord); if (position == FormatSpec.NOT_VALID_WORD) { // TODO: figure out what is the correct thing to do here. } else { @@ -272,7 +273,7 @@ public final class DynamicBinaryDictIOUtils { // find the insert position of the word. if (buffer.position() != 0) buffer.position(0); - final FileHeader header = BinaryDictDecoder.readHeader(buffer); + final FileHeader fileHeader = BinaryDictDecoder.readHeader(dictReader); int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.position(); final int[] codePoints = FusionDictionary.getCodePoints(word); @@ -288,9 +289,9 @@ public final class DynamicBinaryDictIOUtils { for (int i = 0; i < charGroupCount; ++i) { address = buffer.position(); final CharGroupInfo currentInfo = BinaryDictDecoder.readCharGroup(buffer, - buffer.position(), header.mFormatOptions); + buffer.position(), fileHeader.mFormatOptions); final boolean isMovedGroup = BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags, - header.mFormatOptions); + fileHeader.mFormatOptions); if (isMovedGroup) continue; nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address; @@ -310,16 +311,16 @@ public final class DynamicBinaryDictIOUtils { final int newNodeAddress = buffer.limit(); final int flags = BinaryDictEncoder.makeCharGroupFlags(p > 1, isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */, - false /* isBlackListEntry */, header.mFormatOptions); + false /* isBlackListEntry */, fileHeader.mFormatOptions); int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags, frequency, nodeParentAddress, shortcuts, bigrams, destination, - buffer, nodeOriginAddress, address, header.mFormatOptions); + buffer, nodeOriginAddress, address, fileHeader.mFormatOptions); final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p, currentInfo.mCharacters.length); if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { updateParentAddresses(buffer, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, header.mFormatOptions); + newNodeAddress + written + 1, fileHeader.mFormatOptions); } final CharGroupInfo newInfo2 = new CharGroupInfo( newNodeAddress + written + 1, -1 /* endAddress */, @@ -351,17 +352,17 @@ public final class DynamicBinaryDictIOUtils { false /* isTerminal */, 0 /* childrenAddressSize*/, false /* hasShortcut */, false /* hasBigrams */, false /* isNotAWord */, false /* isBlackListEntry */, - header.mFormatOptions); + fileHeader.mFormatOptions); int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, prefixFlags, -1 /* frequency */, nodeParentAddress, null, null, destination, buffer, nodeOriginAddress, address, - header.mFormatOptions); + fileHeader.mFormatOptions); final int[] suffixCharacters = Arrays.copyOfRange( currentInfo.mCharacters, p, currentInfo.mCharacters.length); if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { updateParentAddresses(buffer, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, header.mFormatOptions); + newNodeAddress + written + 1, fileHeader.mFormatOptions); } final int suffixFlags = BinaryDictEncoder.makeCharGroupFlags( suffixCharacters.length > 1, @@ -370,21 +371,21 @@ public final class DynamicBinaryDictIOUtils { (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0, (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0, - isNotAWord, isBlackListEntry, header.mFormatOptions); + isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); final CharGroupInfo suffixInfo = new CharGroupInfo( newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags, suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1, currentInfo.mChildrenAddress, currentInfo.mShortcutTargets, currentInfo.mBigrams); written += BinaryDictIOUtils.computeGroupSize(suffixInfo, - header.mFormatOptions) + 1; + fileHeader.mFormatOptions) + 1; final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p, codePoints.length); final int flags = BinaryDictEncoder.makeCharGroupFlags( newCharacters.length > 1, isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, header.mFormatOptions); + isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); final CharGroupInfo newInfo = new CharGroupInfo( newNodeAddress + written, -1 /* endAddress */, flags, newCharacters, frequency, newNodeAddress + 1, @@ -406,13 +407,13 @@ public final class DynamicBinaryDictIOUtils { final boolean hasMultipleChars = currentInfo.mCharacters.length > 1; final int flags = BinaryDictEncoder.makeCharGroupFlags(hasMultipleChars, isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, header.mFormatOptions); + isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1, -1 /* endAddress */, flags, currentInfo.mCharacters, frequency, nodeParentAddress, currentInfo.mChildrenAddress, shortcuts, bigrams); moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address, - header.mFormatOptions); + fileHeader.mFormatOptions); return; } wordPos += currentInfo.mCharacters.length; @@ -431,12 +432,12 @@ public final class DynamicBinaryDictIOUtils { */ final int newNodeAddress = buffer.limit(); updateChildrenAddress(buffer, address, newNodeAddress, - header.mFormatOptions); + fileHeader.mFormatOptions); final int newGroupAddress = newNodeAddress + 1; final boolean hasMultipleChars = (wordLen - wordPos) > 1; final int flags = BinaryDictEncoder.makeCharGroupFlags(hasMultipleChars, isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, header.mFormatOptions); + isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags, characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS, @@ -481,7 +482,7 @@ public final class DynamicBinaryDictIOUtils { final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); final int flags = BinaryDictEncoder.makeCharGroupFlags(characters.length > 1, isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, header.mFormatOptions); + isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1, -1 /* endAddress */, flags, characters, frequency, nodeParentAddress, FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams); diff --git a/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java b/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java new file mode 100644 index 000000000..7cddef2a4 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 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.makedict.decoder; + +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; + +import java.io.IOException; +import java.util.HashMap; + +/** + * An interface to read a binary dictionary file header. + */ +public interface HeaderReaderInterface { + public int readVersion() throws IOException, UnsupportedFormatException; + public int readOptionFlags(); + public int readHeaderSize(); + public HashMap<String, String> readAttributes(final int headerSize); +} diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java index 065e00e4a..525d3cd11 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java @@ -43,6 +43,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; /** @@ -75,6 +76,8 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = CollectionUtils.newArrayList(); + private final AtomicReference<AsyncTask<Void, Void, Void>> mWaitingTask; + // Should always be false except when we use this class for test @UsedForTesting boolean mIsTest = false; @@ -83,6 +86,7 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona super(context, dictionaryType); mLocale = locale; mPrefs = sp; + mWaitingTask = new AtomicReference<AsyncTask<Void, Void, Void>>(); if (mLocale != null && mLocale.length() > 1) { loadDictionary(); } @@ -174,7 +178,11 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona */ private void flushPendingWrites() { // Create a background thread to write the pending entries - new UpdateBinaryTask(mBigramList, mLocale, this, mPrefs, getContext()).execute(); + final AsyncTask<Void, Void, Void> old = mWaitingTask.getAndSet(new UpdateBinaryTask( + mBigramList, mLocale, this, mPrefs, getContext()).execute()); + if (old != null) { + old.cancel(false); + } } @Override @@ -287,6 +295,7 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona @Override protected Void doInBackground(final Void... v) { + if (isCancelled()) return null; if (mDynamicPredictionDictionary.mIsTest) { // If mIsTest == true, wait until the lock is released. mDynamicPredictionDictionary.mBigramListLock.lock(); @@ -306,6 +315,9 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona } private void doWriteTaskLocked() { + if (isCancelled()) return; + mDynamicPredictionDictionary.mWaitingTask.compareAndSet(this, null); + if (DBG_STRESS_TEST) { try { Log.w(TAG, "Start stress in closing: " + mLocale); diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java index e38a235e9..275ce2fdc 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java @@ -36,7 +36,9 @@ public class PersonalizationDictionary extends ExpandableBinaryDictionary { // Singleton private PersonalizationDictionary(final Context context, final String locale) { - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION); + // TODO: Make isUpdatable true. + super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION, + false /* isUpdatable */); mLocale = locale; } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java index 534d3c518..da59333f5 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java @@ -25,4 +25,10 @@ public class PersonalizationDictionarySessionRegister { public static void onConfigurationChanged(final Context context, final Configuration conf) { } + + public static void onUpdateData(Context context, String type) { + } + + public static void onRemoveData(Context context, String type) { + } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java index 858aa32df..433c69c1c 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin.personalization; +import android.content.Context; + import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -48,7 +50,7 @@ public abstract class PersonalizationDictionaryUpdateSession { public abstract void onDictionaryReady(); - public abstract void onDictionaryClosed(); + public abstract void onDictionaryClosed(Context context); public void setPredictionDictionary(String locale, DynamicPredictionDictionaryBase dictionary) { mPredictionDictionary = new WeakReference<DynamicPredictionDictionaryBase>(dictionary); @@ -68,9 +70,9 @@ public abstract class PersonalizationDictionaryUpdateSession { } - public void closeSession() { + public void closeSession(Context context) { unsetPredictionDictionary(); - onDictionaryClosed(); + onDictionaryClosed(context); } public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid, diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java index 955bd2762..a038d0ab2 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.personalization; import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.ExpandableBinaryDictionary; import android.content.Context; import android.content.SharedPreferences; @@ -31,6 +32,6 @@ public class PersonalizationPredictionDictionary extends DynamicPredictionDictio @Override protected String getDictionaryFileName() { - return NAME + "." + getLocale() + ".dict"; + return NAME + "." + getLocale() + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; } } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java index d11784454..76e48c744 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.personalization; import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.ExpandableBinaryDictionary; import android.content.Context; import android.content.SharedPreferences; @@ -26,7 +27,8 @@ import android.content.SharedPreferences; * cancellation or manual picks. This allows the keyboard to adapt to the typist over time. */ public class UserHistoryPredictionDictionary extends DynamicPredictionDictionaryBase { - private static final String NAME = UserHistoryPredictionDictionary.class.getSimpleName(); + /* package for tests */ static final String NAME = + UserHistoryPredictionDictionary.class.getSimpleName(); /* package */ UserHistoryPredictionDictionary(final Context context, final String locale, final SharedPreferences sp) { super(context, locale, sp, Dictionary.TYPE_USER_HISTORY); @@ -34,6 +36,6 @@ public class UserHistoryPredictionDictionary extends DynamicPredictionDictionary @Override protected String getDictionaryFileName() { - return NAME + "." + getLocale() + ".dict"; + return NAME + "." + getLocale() + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 195f9f8ef..a0b744dd2 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -57,6 +57,7 @@ public final class SettingsValues { public final SuggestedWords mSuggestPuncList; public final String mWordSeparators; public final CharSequence mHintToSaveText; + public final boolean mCurrentLanguageHasSpaces; // From preferences, in the same order as xml/prefs.xml: public final boolean mAutoCap; @@ -118,6 +119,7 @@ public final class SettingsValues { mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); mWordSeparators = res.getString(R.string.symbols_word_separators); mHintToSaveText = res.getText(R.string.hint_add_to_dictionary); + mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); // Store the input attributes if (null == inputAttributes) { @@ -186,6 +188,7 @@ public final class SettingsValues { mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\""; mHintToSaveText = "Touch again to save"; + mCurrentLanguageHasSpaces = true; mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); mAutoCap = true; mVibrateOn = true; diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java index 98f0d8b68..cc25102ce 100644 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils; import android.util.SparseArray; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -94,6 +95,10 @@ public final class CollectionUtils { return new CopyOnWriteArrayList<E>(array); } + public static <E> ArrayDeque<E> newArrayDeque() { + return new ArrayDeque<E>(); + } + public static <E> SparseArray<E> newSparseArray() { return new SparseArray<E>(); } diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 20cc5d815..be4184093 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -370,12 +370,19 @@ public final class StringUtils { return sb.toString(); } + /** + * Convert hex string to byte array. The string length must be an even number. + */ @UsedForTesting public static byte[] hexStringToByteArray(String hexString) { if (TextUtils.isEmpty(hexString)) { return null; } final int N = hexString.length(); + if (N % 2 != 0) { + throw new NumberFormatException("Input hex string length must be an even number." + + " Length = " + N); + } final byte[] bytes = new byte[N / 2]; for (int i = 0; i < N; i += 2) { bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) |