diff options
Diffstat (limited to 'java/src')
47 files changed, 1087 insertions, 378 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java index 7639432aa..c628c5b09 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java @@ -35,6 +35,8 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; @@ -285,9 +287,15 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider private String getKeyDescription(final Key key) { final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo); - final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( + final SettingsValues currentSettings = Settings.getInstance().getCurrent(); + final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey( mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure); - return keyDescription; + if (currentSettings.isWordSeparator(key.getCode())) { + return mAccessibilityUtils.getAutoCorrectionDescription( + keyCodeDescription, shouldObscure); + } else { + return keyCodeDescription; + } } /** diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index 8929dc7e9..10fb9fef4 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -23,6 +23,7 @@ import android.os.Build; import android.os.SystemClock; import android.provider.Settings; import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.text.TextUtils; import android.util.Log; import android.view.MotionEvent; import android.view.View; @@ -34,6 +35,7 @@ import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.SettingsSecureCompatUtils; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.utils.InputTypeUtils; public final class AccessibilityUtils { @@ -48,6 +50,12 @@ public final class AccessibilityUtils { private AccessibilityManager mAccessibilityManager; private AudioManager mAudioManager; + /** The most recent auto-correction. */ + private String mAutoCorrectionWord; + + /** The most recent typed word for auto-correction. */ + private String mTypedWord; + /* * Setting this constant to {@code false} will disable all keyboard * accessibility code, regardless of whether Accessibility is turned on in @@ -142,6 +150,51 @@ public final class AccessibilityUtils { } /** + * Sets the current auto-correction word and typed word. These may be used + * to provide the user with a spoken description of what auto-correction + * will occur when a key is typed. + * + * @param suggestedWords the list of suggested auto-correction words + * @param typedWord the currently typed word + */ + public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { + if (suggestedWords != null && suggestedWords.mWillAutoCorrect) { + mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); + mTypedWord = typedWord; + } else { + mAutoCorrectionWord = null; + mTypedWord = null; + } + } + + /** + * Obtains a description for an auto-correction key, taking into account the + * currently typed word and auto-correction. + * + * @param keyCodeDescription spoken description of the key that will insert + * an auto-correction + * @param shouldObscure whether the key should be obscured + * @return a description including a description of the auto-correction, if + * needed + */ + public String getAutoCorrectionDescription( + final String keyCodeDescription, final boolean shouldObscure) { + if (!TextUtils.isEmpty(mAutoCorrectionWord)) { + if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) { + if (shouldObscure) { + // This should never happen, but just in case... + return mContext.getString(R.string.spoken_auto_correct_obscured, + keyCodeDescription); + } + return mContext.getString(R.string.spoken_auto_correct, keyCodeDescription, + mTypedWord, mAutoCorrectionWord); + } + } + + return keyCodeDescription; + } + + /** * Sends the specified text to the {@link AccessibilityManager} to be * spoken. * diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java index 684165240..c28d72949 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java @@ -24,6 +24,8 @@ import android.preference.PreferenceActivity; * Preference screen. */ public final class DictionarySettingsActivity extends PreferenceActivity { + private static final String DEFAULT_FRAGMENT = DictionarySettingsFragment.class.getName(); + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -32,11 +34,17 @@ public final class DictionarySettingsActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent modIntent = new Intent(super.getIntent()); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DictionarySettingsFragment.class.getName()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); modIntent.putExtra(EXTRA_NO_HEADERS, true); // Important note : the original intent should contain a String extra with the key // DictionarySettingsFragment.DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT so that the // fragment can know who the client is. return modIntent; } + + // TODO: Uncomment the override annotation once we start using SDK version 19. + // @Override + public boolean isValidFragment(String fragmentName) { + return fragmentName.equals(DEFAULT_FRAGMENT); + } } diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java index fed134eb9..e23131a30 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java @@ -50,8 +50,9 @@ public class EmojiCategoryPageIndicatorView extends LinearLayout { @Override protected void onDraw(Canvas canvas) { - if (mCategoryPageSize == 0) { - // If the category is not set yet, just clear and return. + if (mCategoryPageSize <= 1) { + // If the category is not set yet or contains only one category, + // just clear and return. canvas.drawColor(0); return; } diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java index 61dc56ed1..eb48d01f6 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java @@ -718,12 +718,14 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange @Override public void run() { + int repeatCount = 1; int timeCount = 0; while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) { if (timeCount > mKeyRepeatStartTimeout) { - pressDelete(); + pressDelete(repeatCount); } timeCount += mKeyRepeatInterval; + ++repeatCount; try { Thread.sleep(mKeyRepeatInterval); } catch (InterruptedException e) { @@ -736,9 +738,9 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange } } - public void pressDelete() { + public void pressDelete(int repeatCount) { mKeyboardActionListener.onPressKey( - Constants.CODE_DELETE, 0 /* repeatCount */, true /* isSinglePointer */); + Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */); mKeyboardActionListener.onCodeInput( Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE); mKeyboardActionListener.onReleaseKey( @@ -754,7 +756,7 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange switch(event.getAction()) { case MotionEvent.ACTION_DOWN: v.setBackgroundColor(mDeleteKeyPressedBackgroundColor); - pressDelete(); + pressDelete(0 /* repeatCount */); startRepeat(); return true; case MotionEvent.ACTION_UP: diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java index 267fad5cd..71790b7d6 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java @@ -75,9 +75,7 @@ public class EmojiLayoutParams { public void setActionBarProperties(LinearLayout ll) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); - lp.height = mEmojiActionBarHeight; - lp.topMargin = 0; - lp.bottomMargin = mBottomPadding; + lp.height = mEmojiActionBarHeight - mBottomPadding; ll.setLayoutParams(lp); } diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 3ea68806b..f7ec9509d 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -139,6 +139,8 @@ public class Key implements Comparable<Key> { private final OptionalAttributes mOptionalAttributes; + private static final int DEFAULT_TEXT_COLOR = 0xFFFFFFFF; + private static final class OptionalAttributes { /** Text to output when pressed. This can be multiple characters, like ".com" */ public final String mOutputText; @@ -602,7 +604,22 @@ public class Key implements Comparable<Key> { } public final int selectTextColor(final KeyDrawParams params) { - return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; + if (isShiftedLetterActivated()) { + return params.mTextInactivatedColor; + } + if (params.mTextColorStateList == null) { + return DEFAULT_TEXT_COLOR; + } + final int[] state; + // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels. + // Currently, we distinguish "input key" from "functional key" by checking the + // length of the label( > 1) and "functional" attributes (= true). + if (mLabel != null && mLabel.length() > 1) { + state = getCurrentDrawableState(); + } else { + state = KEY_STATE_NORMAL; + } + return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR); } public final int selectHintTextSize(final KeyDrawParams params) { diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index cc1ffd183..ad6e2c0f2 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -155,7 +155,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } public void saveKeyboardState() { - if (getKeyboard() != null) { + if (getKeyboard() != null || isShowingEmojiKeyboard()) { mState.onSaveKeyboardState(); } } @@ -315,7 +315,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } public boolean isShowingEmojiKeyboard() { - return mEmojiKeyboardView.getVisibility() == View.VISIBLE; + return mEmojiKeyboardView != null && mEmojiKeyboardView.getVisibility() == View.VISIBLE; } public boolean isShowingMoreKeysPanel() { diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index ee4ac950c..52f190e77 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -823,14 +823,16 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int size = sAggregratedPointers.getPointerSize(); if (size > sLastRecognitionPointSize && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { - sLastRecognitionPointSize = size; - sLastRecognitionTime = eventTime; if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, size)); } mTimerProxy.startUpdateBatchInputTimer(this); mListener.onUpdateBatchInput(sAggregratedPointers); + // The listener may change the size of the pointers (when auto-committing + // for example), so we need to get the size from the pointers again. + sLastRecognitionPointSize = sAggregratedPointers.getPointerSize(); + sLastRecognitionTime = eventTime; } } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java index c10fdbace..4ccecb2f0 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java @@ -18,6 +18,8 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.Constants; +import android.text.TextUtils; + /** * The string parser of codesArray specification for <GridRows />. The attribute codesArray is an * array of string. @@ -34,7 +36,7 @@ import com.android.inputmethod.latin.Constants; public final class CodesArrayParser { // Constants for parsing. private static final char COMMA = ','; - private static final char VERTICAL_BAR = '|'; + private static final String VERTICAL_BAR_STRING = "\\|"; private static final String COMMA_STRING = ","; private static final int BASE_HEX = 16; @@ -43,8 +45,11 @@ public final class CodesArrayParser { } private static String getLabelSpec(final String codesArraySpec) { - final int pos = codesArraySpec.indexOf(VERTICAL_BAR); - return (pos < 0) ? codesArraySpec : codesArraySpec.substring(0, pos); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + if (strs.length <= 1) { + return codesArraySpec; + } + return strs[0]; } public static String parseLabel(final String codesArraySpec) { @@ -58,8 +63,25 @@ public final class CodesArrayParser { } private static String getCodeSpec(final String codesArraySpec) { - final int pos = codesArraySpec.indexOf(VERTICAL_BAR); - return (pos < 0) ? codesArraySpec : codesArraySpec.substring(pos + 1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + if (strs.length <= 1) { + return codesArraySpec; + } + return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1]; + } + + // codesArraySpec consists of: + // <label>|<code0>,<code1>,...|<minSupportSdkVersion> + public static int getMinSupportSdkVersion(final String codesArraySpec) { + final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + if (strs.length <= 2) { + return 0; + } + try { + return Integer.parseInt(strs[2]); + } catch (NumberFormatException e) { + return 0; + } } public static int parseCode(final String codesArraySpec) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java index 1716fa049..b528b692e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java @@ -16,6 +16,7 @@ package com.android.inputmethod.keyboard.internal; +import android.content.res.ColorStateList; import android.graphics.Typeface; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -32,7 +33,7 @@ public final class KeyDrawParams { public int mHintLabelSize; public int mPreviewTextSize; - public int mTextColor; + public ColorStateList mTextColorStateList; public int mTextInactivatedColor; public int mTextShadowColor; public int mHintLetterColor; @@ -57,7 +58,7 @@ public final class KeyDrawParams { mHintLabelSize = copyFrom.mHintLabelSize; mPreviewTextSize = copyFrom.mPreviewTextSize; - mTextColor = copyFrom.mTextColor; + mTextColorStateList = copyFrom.mTextColorStateList; mTextInactivatedColor = copyFrom.mTextInactivatedColor; mTextShadowColor = copyFrom.mTextShadowColor; mHintLetterColor = copyFrom.mHintLetterColor; @@ -89,8 +90,8 @@ public final class KeyDrawParams { attr.mShiftedLetterHintRatio, mShiftedLetterHintSize); mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize); mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize); - - mTextColor = selectColor(attr.mTextColor, mTextColor); + mTextColorStateList = + attr.mTextColorStateList != null ? attr.mTextColorStateList : mTextColorStateList; mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java index 7a2622cbb..8bdad364c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -16,6 +16,7 @@ package com.android.inputmethod.keyboard.internal; +import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Typeface; import android.util.SparseIntArray; @@ -37,7 +38,7 @@ public final class KeyVisualAttributes { public final float mHintLabelRatio; public final float mPreviewTextRatio; - public final int mTextColor; + public final ColorStateList mTextColorStateList; public final int mTextInactivatedColor; public final int mTextShadowColor; public final int mHintLetterColor; @@ -115,7 +116,7 @@ public final class KeyVisualAttributes { mPreviewTextRatio = ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyPreviewTextRatio); - mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0); + mTextColorStateList = keyAttr.getColorStateList(R.styleable.Keyboard_Key_keyTextColor); mTextInactivatedColor = keyAttr.getColor( R.styleable.Keyboard_Key_keyTextInactivatedColor, 0); mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index 22f7a83fc..c1ae65695 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -436,17 +437,24 @@ public class KeyboardBuilder<KP extends KeyboardParams> { final String label; final int code; final String outputText; + final int supportedMinSdkVersion; if (codesArrayId != 0) { final String codeArraySpec = array[i]; label = CodesArrayParser.parseLabel(codeArraySpec); code = CodesArrayParser.parseCode(codeArraySpec); outputText = CodesArrayParser.parseOutputText(codeArraySpec); + supportedMinSdkVersion = + CodesArrayParser.getMinSupportSdkVersion(codeArraySpec); } else { final String textArraySpec = array[i]; // TODO: Utilize KeySpecParser or write more generic TextsArrayParser. label = textArraySpec; code = Constants.CODE_OUTPUT_TEXT; outputText = textArraySpec + (char)Constants.CODE_SPACE; + supportedMinSdkVersion = 0; + } + if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { + continue; } final int x = (int)row.getKeyX(null); final int y = row.getKeyY(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index 9f9fdaa6f..506dfa751 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -178,6 +178,8 @@ public final class KeyboardState { if (!state.mIsAlphabetShiftLocked) { setShifted(state.mShiftMode); } + // TODO: is this the right place to do this? Should we do this in setShift* instead? + mSwitchActions.requestUpdatingShiftState(); } else { mPrevMainKeyboardWasShiftLocked = state.mIsAlphabetShiftLocked; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index 67553fb75..684cf632b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -209,8 +209,8 @@ public final class KeyboardTextsSet { /* 104 */ "keylabel_for_tablet_comma", /* 105 */ "keyhintlabel_for_tablet_comma", /* 106 */ "more_keys_for_tablet_comma", - /* 107 */ "keyhintlabel_for_tablet_period", - /* 108 */ "more_keys_for_tablet_period", + /* 107 */ "keyhintlabel_for_period", + /* 108 */ "more_keys_for_period", /* 109 */ "keylabel_for_apostrophe", /* 110 */ "keyhintlabel_for_apostrophe", /* 111 */ "more_keys_for_apostrophe", diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java index 845a9b987..4a0ce3735 100644 --- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java +++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java @@ -25,6 +25,7 @@ import com.android.inputmethod.latin.makedict.Ver3DictEncoder; import java.io.File; import java.io.IOException; +import java.util.Map; // TODO: Quit extending Dictionary after implementing dynamic binary dictionary. abstract public class AbstractDictionaryWriter extends Dictionary { @@ -50,16 +51,16 @@ abstract public class AbstractDictionaryWriter extends Dictionary { abstract public void removeBigramWords(final String word0, final String word1); - abstract protected void writeDictionary(final DictEncoder dictEncoder) - throws IOException, UnsupportedFormatException; + abstract protected void writeDictionary(final DictEncoder dictEncoder, + final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException; - public void write(final String fileName) { + public void write(final String fileName, final Map<String, String> attributeMap) { final String tempFileName = fileName + ".temp"; final File file = new File(mContext.getFilesDir(), fileName); final File tempFile = new File(mContext.getFilesDir(), tempFileName); try { final DictEncoder dictEncoder = new Ver3DictEncoder(tempFile); - writeDictionary(dictEncoder); + writeDictionary(dictEncoder, attributeMap); tempFile.renameTo(file); } catch (IOException e) { Log.e(TAG, "IO exception while writing file", e); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index a463651d5..541e69788 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -44,9 +44,14 @@ public final class BinaryDictionary extends Dictionary { private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; // Must be equal to MAX_RESULTS in native/jni/src/defines.h private static final int MAX_RESULTS = 18; - // Required space count for auto commit. - // TODO: Remove this heuristic. - private static final int SPACE_COUNT_FOR_AUTO_COMMIT = 3; + // The cutoff returned by native for auto-commit confidence. + // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h + private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000; + + @UsedForTesting + public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT"; + @UsedForTesting + public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT"; private long mNativeDict; private final Locale mLocale; @@ -57,7 +62,8 @@ public final class BinaryDictionary extends Dictionary { private final int[] mSpaceIndices = new int[MAX_RESULTS]; private final int[] mOutputScores = new int[MAX_RESULTS]; private final int[] mOutputTypes = new int[MAX_RESULTS]; - private final int[] mOutputAutoCommitFirstWordConfidence = new int[MAX_RESULTS]; + // Only one result is ever used + private final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); @@ -110,7 +116,7 @@ public final class BinaryDictionary extends Dictionary { private static native long openNative(String sourceDir, long dictOffset, long dictSize, boolean isUpdatable); private static native void flushNative(long dict, String filePath); - private static native boolean needsToRunGCNative(long dict); + private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC); private static native void flushWithGCNative(long dict, String filePath); private static native void closeNative(long dict); private static native int getProbabilityNative(long dict, int[] word); @@ -129,6 +135,7 @@ public final class BinaryDictionary extends Dictionary { private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1); private static native int calculateProbabilityNative(long dict, int unigramProbability, int bigramProbability); + private static native String getPropertyNative(long dict, String query); @UsedForTesting public static boolean createEmptyDictFile(final String filePath, final long dictVersion, @@ -263,18 +270,11 @@ public final class BinaryDictionary extends Dictionary { return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1); } - private void runGCIfRequired() { - if (needsToRunGCNative(mNativeDict)) { - flushWithGC(); - } - } - // Add a unigram entry to binary dictionary in native code. public void addUnigramWord(final String word, final int probability) { if (TextUtils.isEmpty(word)) { return; } - runGCIfRequired(); final int[] codePoints = StringUtils.toCodePointArray(word); addUnigramWordNative(mNativeDict, codePoints, probability); } @@ -284,7 +284,6 @@ public final class BinaryDictionary extends Dictionary { if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { return; } - runGCIfRequired(); final int[] codePoints0 = StringUtils.toCodePointArray(word0); final int[] codePoints1 = StringUtils.toCodePointArray(word1); addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability); @@ -295,7 +294,6 @@ public final class BinaryDictionary extends Dictionary { if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { return; } - runGCIfRequired(); final int[] codePoints0 = StringUtils.toCodePointArray(word0); final int[] codePoints1 = StringUtils.toCodePointArray(word1); removeBigramWordsNative(mNativeDict, codePoints0, codePoints1); @@ -320,9 +318,15 @@ public final class BinaryDictionary extends Dictionary { reopen(); } - public boolean needsToRunGC() { + /** + * Checks whether GC is needed to run or not. + * @param mindsBlockByGC Whether to mind operations blocked by GC. We don't need to care about + * the blocking in some situations such as in idle time or just before closing. + * @return whether GC is needed to run or not. + */ + public boolean needsToRunGC(final boolean mindsBlockByGC) { if (!isValidDictionary()) return false; - return needsToRunGCNative(mNativeDict); + return needsToRunGCNative(mNativeDict, mindsBlockByGC); } @UsedForTesting @@ -331,20 +335,15 @@ public final class BinaryDictionary extends Dictionary { return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability); } + @UsedForTesting + public String getPropertyForTests(String query) { + if (!isValidDictionary()) return ""; + return getPropertyNative(mNativeDict, query); + } + @Override public boolean shouldAutoCommit(final SuggestedWordInfo candidate) { - // TODO: actually use the confidence rather than use this completely broken heuristic - final String word = candidate.mWord; - final int length = word.length(); - int remainingSpaces = SPACE_COUNT_FOR_AUTO_COMMIT; - for (int i = 0; i < length; ++i) { - // This is okay because no low-surrogate and no high-surrogate can ever match the - // space character, so we don't need to take care of iterating on code points. - if (Constants.CODE_SPACE == word.charAt(i)) { - if (0 >= --remainingSpaces) return true; - } - } - return false; + return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT; } @Override diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 3721132c5..828e54f14 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -51,7 +51,7 @@ public final class DictionaryFactory { if (null == locale) { Log.e(TAG, "No locale defined for dictionary"); return new DictionaryCollection(Dictionary.TYPE_MAIN, - createBinaryDictionary(context, locale)); + createReadOnlyBinaryDictionary(context, locale)); } final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList(); @@ -59,11 +59,11 @@ public final class DictionaryFactory { BinaryDictionaryGetter.getDictionaryFiles(locale, context); if (null != assetFileList) { for (final AssetFileAddress f : assetFileList) { - final BinaryDictionary binaryDictionary = new BinaryDictionary(f.mFilename, - f.mOffset, f.mLength, useFullEditDistance, locale, Dictionary.TYPE_MAIN, - false /* isUpdatable */); - if (binaryDictionary.isValidDictionary()) { - dictList.add(binaryDictionary); + final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = + new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength, + useFullEditDistance, locale, Dictionary.TYPE_MAIN); + if (readOnlyBinaryDictionary.isValidDictionary()) { + dictList.add(readOnlyBinaryDictionary); } } } @@ -89,12 +89,12 @@ public final class DictionaryFactory { } /** - * Initializes a dictionary from a raw resource file + * Initializes a read-only binary dictionary from a raw resource file * @param context application context for reading resources * @param locale the locale to use for the resource - * @return an initialized instance of BinaryDictionary + * @return an initialized instance of ReadOnlyBinaryDictionary */ - protected static BinaryDictionary createBinaryDictionary(final Context context, + protected static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context, final Locale locale) { AssetFileDescriptor afd = null; try { @@ -113,9 +113,8 @@ public final class DictionaryFactory { Log.e(TAG, "sourceDir is not a file: " + sourceDir); return null; } - return new BinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(), - false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN, - false /* isUpdatable */); + return new ReadOnlyBinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(), + false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN); } catch (android.content.res.Resources.NotFoundException e) { Log.e(TAG, "Could not find the resource"); return null; @@ -142,10 +141,10 @@ public final class DictionaryFactory { final DictionaryCollection dictionaryCollection = new DictionaryCollection(Dictionary.TYPE_MAIN); for (final AssetFileAddress address : dictionaryList) { - final BinaryDictionary binaryDictionary = new BinaryDictionary(address.mFilename, - address.mOffset, address.mLength, useFullEditDistance, locale, - Dictionary.TYPE_MAIN, false /* isUpdatable */); - dictionaryCollection.addDictionary(binaryDictionary); + final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary( + address.mFilename, address.mOffset, address.mLength, useFullEditDistance, + locale, Dictionary.TYPE_MAIN); + dictionaryCollection.addDictionary(readOnlyBinaryDictionary); } return dictionaryCollection; } diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java index 5a453dde5..84abfa66d 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java +++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java @@ -31,6 +31,7 @@ import com.android.inputmethod.latin.utils.CollectionUtils; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; /** * An in memory dictionary for memorizing entries and writing a binary dictionary. @@ -84,8 +85,11 @@ public class DictionaryWriter extends AbstractDictionaryWriter { } @Override - protected void writeDictionary(final DictEncoder dictEncoder) - throws IOException, UnsupportedFormatException { + protected void writeDictionary(final DictEncoder dictEncoder, + final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { + for (final Map.Entry<String, String> entry : attributeMap.entrySet()) { + mFusionDictionary.addOptionAttribute(entry.getKey(), entry.getValue()); + } dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS); } diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index dc4b0a0cc..c79a4ff90 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -236,6 +236,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { HashMap<String, String> attributeMap = new HashMap<String, String>(); attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, SUPPORTS_DYNAMIC_UPDATE); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename); return attributeMap; } @@ -248,6 +249,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { final File file = new File(mContext.getFilesDir(), mFilename); BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + mBinaryDictionary = new BinaryDictionary( + file.getAbsolutePath(), 0 /* offset */, file.length(), + true /* useFullEditDistance */, null, mDictType, mIsUpdatable); } else { mDictionaryWriter.clear(); } @@ -273,6 +277,39 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } /** + * Check whether GC is needed and run GC if required. + */ + protected void runGCIfRequired(final boolean mindsBlockByGC) { + if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + runGCIfRequiredInternalLocked(mindsBlockByGC); + } + }); + } + + private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) { + if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; + // Calls to needsToRunGC() need to be serialized. + if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { + if (setIsRegeneratingIfNotRegenerating()) { + // Run GC after currently existing time sensitive operations. + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + try { + mBinaryDictionary.flushWithGC(); + } finally { + mFilenameDictionaryUpdateController.mIsRegenerating.set(false); + } + } + }); + } + } + } + + /** * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry. */ protected void addWordDynamically(final String word, final String shortcutTarget, @@ -281,11 +318,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename); return; } - getExecutor(mFilename).execute(new Runnable() { @Override public void run() { if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); mBinaryDictionary.addUnigramWord(word, frequency); } else { // TODO: Remove. @@ -305,11 +342,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { + mFilename); return; } - getExecutor(mFilename).execute(new Runnable() { @Override public void run() { if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); mBinaryDictionary.addBigramWords(word0, word1, frequency); } else { // TODO: Remove. @@ -329,11 +366,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { + mFilename); return; } - getExecutor(mFilename).execute(new Runnable() { @Override public void run() { if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); mBinaryDictionary.removeBigramWords(word0, word1); } else { // TODO: Remove. @@ -460,8 +497,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { final long length = file.length(); // Build the new binary dictionary - final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length, - true /* useFullEditDistance */, null, mDictType, mIsUpdatable); + final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0 /* offset */, + length, true /* useFullEditDistance */, null, mDictType, mIsUpdatable); // Ensure all threads accessing the current dictionary have finished before // swapping in the new one. @@ -496,7 +533,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { if (needsToReloadBeforeWriting()) { mDictionaryWriter.clear(); loadDictionaryAsync(); - mDictionaryWriter.write(mFilename); + mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); } else { if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) { @@ -504,14 +541,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); } else { - if (mBinaryDictionary.needsToRunGC()) { + if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { mBinaryDictionary.flushWithGC(); } else { mBinaryDictionary.flush(); } } } else { - mDictionaryWriter.write(mFilename); + mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); } } } @@ -660,47 +697,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } } - /** - * 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) { - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - addWordDynamically(word, shortcutTarget, frequency, isNotAWord); - } - }); - } - - /** - * 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) { - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - addBigramDynamically(word0, word1, frequency, isValid); - } - }); - } - - /** - * Dynamically remove a word bigram in the dictionary for testing with blocking-lock. - */ - @UsedForTesting - protected void removeBigramDynamicallyForTests(final String word0, final String word1) { - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - removeBigramDynamically(word0, word1); - } - }); - } - // TODO: Implement native binary methods once the dynamic dictionary implementation is done. @UsedForTesting public boolean isInDictionaryForTests(final String word) { diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index 21b103e5a..8caf6f17f 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -103,6 +103,10 @@ public final class InputAttributes { } } + public boolean isTypeNull() { + return InputType.TYPE_NULL == mInputType; + } + public boolean isSameInputType(final EditorInfo editorInfo) { return editorInfo.inputType == mInputType; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 270dc4c06..0f3d28976 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -77,11 +77,12 @@ import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; import com.android.inputmethod.latin.personalization.PersonalizationDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; import com.android.inputmethod.latin.personalization.PersonalizationHelper; import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary; +import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.settings.SettingsValues; @@ -179,7 +180,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mIsMainDictionaryAvailable; private UserBinaryDictionary mUserDictionary; - private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary; + private UserHistoryDictionary mUserHistoryDictionary; private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary; private PersonalizationDictionary mPersonalizationDictionary; private boolean mIsUserDictionaryAvailable; @@ -567,6 +568,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); + mInputUpdater = new InputUpdater(this); } @@ -623,9 +626,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - mUserHistoryPredictionDictionary = PersonalizationHelper - .getUserHistoryPredictionDictionary(this, localeStr, prefs); - newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary); + mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary( + this, localeStr, prefs); + newSuggest.setUserHistoryDictionary(mUserHistoryDictionary); mPersonalizationDictionary = PersonalizationHelper .getPersonalizationDictionary(this, localeStr, prefs); newSuggest.setPersonalizationDictionary(mPersonalizationDictionary); @@ -1521,7 +1524,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeState.switchSubtype(token, mRichImm); } - private void sendDownUpKeyEventForBackwardCompatibility(final int code) { + private void sendDownUpKeyEvent(final int code) { final long eventTime = SystemClock.uptimeMillis(); mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, @@ -1538,7 +1541,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Remove this special handling of digit letters. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. if (code >= '0' && code <= '9') { - sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0); + sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0); return; } @@ -1547,7 +1550,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // a hardware keyboard event on pressing enter or delete. This is bad for many // reasons (there are race conditions with commits) but some applications are // relying on this behavior so we continue to support it for older apps. - sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER); + sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); } else { final String text = new String(new int[] { code }, 0, 1); mConnection.commitText(text, text.length()); @@ -2104,12 +2107,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } final int lengthToDelete = Character.isSupplementaryCodePoint( mConnection.getCodePointBeforeCursor()) ? 2 : 1; - if (mAppWorkAroundsUtils.isBeforeJellyBean()) { - // Backward compatibility mode. Before Jelly bean, the keyboard would simulate - // a hardware keyboard event on pressing enter or delete. This is bad for many - // reasons (there are race conditions with commits) but some applications are - // relying on this behavior so we continue to support it for older apps. - sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); + if (mAppWorkAroundsUtils.isBeforeJellyBean() || + currentSettings.mInputAttributes.isTypeNull()) { + // There are two possible reasons to send a key event: either the field has + // type TYPE_NULL, in which case the keyboard should send events, or we are + // running in backward compatibility mode. Before Jelly bean, the keyboard + // would simulate a hardware keyboard event on pressing enter or delete. This + // is bad for many reasons (there are race conditions with commits) but some + // applications are relying on this behavior so we continue to support it for + // older apps, so we retain this behavior if the app has target SDK < JellyBean. + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); } else { mConnection.deleteSurroundingText(lengthToDelete, 0); } @@ -2553,6 +2560,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, final String typedWord) { if (suggestedWords.isEmpty()) { + // No auto-correction is available, clear the cached values. + AccessibilityUtils.getInstance().setAutoCorrection(null, null); clearSuggestionStrip(); return; } @@ -2561,6 +2570,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setSuggestedWords(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(isSuggestionsStripVisible()); + // An auto-correction is available, cache it in accessibility code so + // we can be speak it if the user touches a key that will insert it. + AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord); } private void showSuggestionStrip(final SuggestedWords suggestedWords) { @@ -2746,9 +2758,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SettingsValues currentSettings = mSettings.getCurrent(); if (!currentSettings.mCorrectionEnabled) return null; - final UserHistoryPredictionDictionary userHistoryPredictionDictionary = - mUserHistoryPredictionDictionary; - if (userHistoryPredictionDictionary == null) return null; + final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; + if (userHistoryDictionary == null) return null; final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2); final String secondWord; @@ -2762,8 +2773,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int maxFreq = AutoCorrectionUtils.getMaxFrequency( suggest.getUnigramDictionaries(), suggestion); if (maxFreq == 0) return null; - userHistoryPredictionDictionary - .addToPersonalizationPredictionDictionary(prevWord, secondWord, maxFreq > 0); + userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0); return prevWord; } @@ -2922,6 +2932,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } tryFixLyingCursorPosition(); + mKeyboardSwitcher.updateShiftState(); if (tryResumeSuggestions) mHandler.postResumeSuggestions(); } @@ -2949,7 +2960,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { - mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord); + mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); } final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java new file mode 100644 index 000000000..68505ce38 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java @@ -0,0 +1,120 @@ +/* + * 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; + +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class provides binary dictionary reading operations with locking. An instance of this class + * can be used by multiple threads. Note that different session IDs must be used when multiple + * threads get suggestions using this class. + */ +public final class ReadOnlyBinaryDictionary extends Dictionary { + /** + * A lock for accessing binary dictionary. Only closing binary dictionary is the operation + * that change the state of dictionary. + */ + private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); + + private final BinaryDictionary mBinaryDictionary; + + public ReadOnlyBinaryDictionary(final String filename, final long offset, final long length, + final boolean useFullEditDistance, final Locale locale, final String dictType) { + super(dictType); + mBinaryDictionary = new BinaryDictionary(filename, offset, length, useFullEditDistance, + locale, dictType, false /* isUpdatable */); + } + + public boolean isValidDictionary() { + return mBinaryDictionary.isValidDictionary(); + } + + @Override + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, + final String prevWord, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, 0 /* sessionId */); + } + + @Override + public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, + final String prevWord, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId) { + if (mLock.readLock().tryLock()) { + try { + return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions); + } finally { + mLock.readLock().unlock(); + } + } + return null; + } + + @Override + public boolean isValidWord(final String word) { + if (mLock.readLock().tryLock()) { + try { + return mBinaryDictionary.isValidWord(word); + } finally { + mLock.readLock().unlock(); + } + } + return false; + } + + @Override + public boolean shouldAutoCommit(final SuggestedWordInfo candidate) { + if (mLock.readLock().tryLock()) { + try { + return mBinaryDictionary.shouldAutoCommit(candidate); + } finally { + mLock.readLock().unlock(); + } + } + return false; + } + + @Override + public int getFrequency(final String word) { + if (mLock.readLock().tryLock()) { + try { + return mBinaryDictionary.getFrequency(word); + } finally { + mLock.readLock().unlock(); + } + } + return NOT_A_PROBABILITY; + } + + @Override + public void close() { + mLock.writeLock().lock(); + try { + mBinaryDictionary.close(); + } finally { + mLock.writeLock().unlock(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 6c18c948f..9fd1f53a2 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -26,7 +26,7 @@ import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.personalization.PersonalizationDictionary; import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary; +import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.BoundedTreeSet; @@ -190,10 +190,8 @@ public final class Suggest { addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary); } - public void setUserHistoryPredictionDictionary( - final UserHistoryPredictionDictionary userHistoryPredictionDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, - userHistoryPredictionDictionary); + public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) { + addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary); } public void setPersonalizationPredictionDictionary( diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java index 665c7a27c..2c3d1346f 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java @@ -295,7 +295,6 @@ public final class BinaryDictDecoderUtils { return address; } } - int address; switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: return dictBuffer.readUnsignedByte(); diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java index 6cc0bfb76..b6024243f 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java @@ -278,7 +278,6 @@ public class BinaryDictEncoderUtils { // For future reference, the code to remove duplicate is a simple : list.remove(node); list.add(ptNodeArray); final ArrayList<PtNode> branches = ptNodeArray.mData; - final int nodeSize = branches.size(); for (PtNode ptNode : branches) { if (null != ptNode.mChildren) flattenTreeInner(list, ptNode.mChildren); } @@ -385,12 +384,14 @@ public class BinaryDictEncoderUtils { nodeSize + size, ptNode.mChildren)); } nodeSize += getShortcutListSize(ptNode.mShortcutTargets); - if (null != ptNode.mBigrams) { - for (WeightedString bigram : ptNode.mBigrams) { - final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray, - nodeSize + size + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE, - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord)); - nodeSize += getByteSize(offset) + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; + if (formatOptions.mVersion < FormatSpec.FIRST_VERSION_WITH_TERMINAL_ID) { + if (null != ptNode.mBigrams) { + for (WeightedString bigram : ptNode.mBigrams) { + final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray, + nodeSize + size + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE, + FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord)); + nodeSize += getByteSize(offset) + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; + } } } ptNode.mCachedSize = nodeSize; @@ -425,9 +426,6 @@ public class BinaryDictEncoderUtils { nodeCountSize + nodeArrayOffset + nodeffset; nodeffset += ptNode.mCachedSize; } - final int nodeSize = nodeCountSize + nodeffset - + (formatOptions.mSupportsDynamicUpdate - ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0); nodeArrayOffset += nodeArray.mCachedSize; } return nodeArrayOffset; @@ -651,8 +649,8 @@ public class BinaryDictEncoderUtils { return flags; } - /* package */ static byte makePtNodeFlags(final PtNode node, final int ptNodeAddress, - final int childrenOffset, final FormatOptions formatOptions) { + /* package */ static byte makePtNodeFlags(final PtNode node, final int childrenOffset, + final FormatOptions formatOptions) { return (byte) makePtNodeFlags(node.mChars.length > 1, node.mFrequency >= 0, getByteSize(childrenOffset), node.mShortcutTargets != null && !node.mShortcutTargets.isEmpty(), diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java index a282f595c..0f7d2f6c9 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -288,40 +288,6 @@ public final class BinaryDictIOUtils { return BinaryDictEncoderUtils.getByteSize(value); } - static void skipPtNode(final DictBuffer dictBuffer, final FormatOptions formatOptions) { - final int flags = dictBuffer.readUnsignedByte(); - BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions); - skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - BinaryDictDecoderUtils.readChildrenAddress(dictBuffer, flags, formatOptions); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte(); - if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) { - final int shortcutsSize = dictBuffer.readUnsignedShort(); - dictBuffer.position(dictBuffer.position() + shortcutsSize - - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); - } - if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) { - int bigramCount = 0; - while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - final int bigramFlags = dictBuffer.readUnsignedByte(); - switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: - dictBuffer.readUnsignedByte(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: - dictBuffer.readUnsignedShort(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: - dictBuffer.readUnsignedInt24(); - break; - } - if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break; - } - if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode."); - } - } - } - static void skipString(final DictBuffer dictBuffer, final boolean hasMultipleChars) { if (hasMultipleChars) { diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java index 3796a466c..e251f7df7 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java @@ -391,4 +391,6 @@ public abstract class DictDecoder { return readLength; } } + + public abstract void skipPtNode(final FormatOptions formatOptions); } diff --git a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java new file mode 100644 index 000000000..413d0301c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java @@ -0,0 +1,50 @@ +/* + * 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; + +import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * An interface of a binary dictionary updater. + */ +public interface DictUpdater { + + /** + * Deletes the word from the binary dictionary. + * + * @param word the word to be deleted. + */ + public void deleteWord(final String word) throws IOException, UnsupportedFormatException; + + /** + * Inserts a word into a binary dictionary. + * + * @param word the word to be inserted. + * @param frequency the frequency of the new word. + * @param bigramStrings bigram list, or null if none. + * @param shortcuts shortcut list, or null if none. + * @param isBlackListEntry whether this should be a blacklist entry. + */ + // TODO: Support batch insertion. + public void insertWord(final String word, final int frequency, + final ArrayList<WeightedString> bigramStrings, + final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, + final boolean isBlackListEntry) throws IOException, UnsupportedFormatException; +} diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java index bf3d19101..336277196 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java @@ -42,44 +42,22 @@ public final class DynamicBinaryDictIOUtils { // This utility class is not publicly instantiable. } - private static int markAsDeleted(final int flags) { + /* package */ static int markAsDeleted(final int flags) { return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED; } /** - * Delete the word from the binary file. - * - * @param dictDecoder the dict decoder. - * @param word the word we delete - * @throws IOException - * @throws UnsupportedFormatException - */ - @UsedForTesting - public static void deleteWord(final Ver3DictDecoder dictDecoder, final String word) - throws IOException, UnsupportedFormatException { - final DictBuffer dictBuffer = dictDecoder.getDictBuffer(); - dictBuffer.position(0); - final FileHeader header = dictDecoder.readHeader(); - final int wordPosition = dictDecoder.getTerminalPosition(word); - if (wordPosition == FormatSpec.NOT_VALID_WORD) return; - - dictBuffer.position(wordPosition); - final int flags = dictBuffer.readUnsignedByte(); - dictBuffer.position(wordPosition); - dictBuffer.put((byte)markAsDeleted(flags)); - } - - /** * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress. * - * @param dictBuffer the DictBuffer to write. + * @param dictUpdater the DictUpdater to write. * @param ptNodeOriginAddress the address of the PtNode. * @param newParentAddress the absolute address of the parent. * @param formatOptions file format options. */ - public static void updateParentAddress(final DictBuffer dictBuffer, + private static void updateParentAddress(final Ver3DictUpdater dictUpdater, final int ptNodeOriginAddress, final int newParentAddress, final FormatOptions formatOptions) { + final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); final int originalPosition = dictBuffer.position(); dictBuffer.position(ptNodeOriginAddress); if (!formatOptions.mSupportsDynamicUpdate) { @@ -104,46 +82,45 @@ public final class DynamicBinaryDictIOUtils { /** * Update parent addresses in a node array stored at ptNodeOriginAddress. * - * @param dictBuffer the DictBuffer to be modified. + * @param dictUpdater the DictUpdater to be modified. * @param ptNodeOriginAddress the address of the node array to update. * @param newParentAddress the address to be written. * @param formatOptions file format options. */ - public static void updateParentAddresses(final DictBuffer dictBuffer, + private static void updateParentAddresses(final Ver3DictUpdater dictUpdater, final int ptNodeOriginAddress, final int newParentAddress, final FormatOptions formatOptions) { - final int originalPosition = dictBuffer.position(); - dictBuffer.position(ptNodeOriginAddress); + final int originalPosition = dictUpdater.getPosition(); + dictUpdater.setPosition(ptNodeOriginAddress); do { - final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); + final int count = dictUpdater.readPtNodeCount(); for (int i = 0; i < count; ++i) { - updateParentAddress(dictBuffer, dictBuffer.position(), newParentAddress, + updateParentAddress(dictUpdater, dictUpdater.getPosition(), newParentAddress, formatOptions); - BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions); + dictUpdater.skipPtNode(formatOptions); } - final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); - dictBuffer.position(forwardLinkAddress); - } while (formatOptions.mSupportsDynamicUpdate - && dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS); - dictBuffer.position(originalPosition); + if (!dictUpdater.readAndFollowForwardLink()) break; + if (dictUpdater.getPosition() == FormatSpec.NO_FORWARD_LINK_ADDRESS) break; + } while (formatOptions.mSupportsDynamicUpdate); + dictUpdater.setPosition(originalPosition); } /** * Update a children address in a PtNode that is addressed by ptNodeOriginAddress. * - * @param dictBuffer the DictBuffer to write. + * @param dictUpdater the DictUpdater to write. * @param ptNodeOriginAddress the address of the PtNode. * @param newChildrenAddress the absolute address of the child. * @param formatOptions file format options. */ - public static void updateChildrenAddress(final DictBuffer dictBuffer, + private static void updateChildrenAddress(final Ver3DictUpdater dictUpdater, final int ptNodeOriginAddress, final int newChildrenAddress, final FormatOptions formatOptions) { + final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); final int originalPosition = dictBuffer.position(); dictBuffer.position(ptNodeOriginAddress); final int flags = dictBuffer.readUnsignedByte(); - final int parentAddress = BinaryDictDecoderUtils.readParentAddress(dictBuffer, - formatOptions); + BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions); BinaryDictIOUtils.skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte(); final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS @@ -156,31 +133,33 @@ public final class DynamicBinaryDictIOUtils { * Helper method to move a PtNode to the tail of the file. */ private static int movePtNode(final OutputStream destination, - final DictBuffer dictBuffer, final PtNodeInfo info, + final Ver3DictUpdater dictUpdater, final PtNodeInfo info, final int nodeArrayOriginAddress, final int oldNodeAddress, final FormatOptions formatOptions) throws IOException { - updateParentAddress(dictBuffer, oldNodeAddress, dictBuffer.limit() + 1, formatOptions); + final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); + updateParentAddress(dictUpdater, oldNodeAddress, dictBuffer.limit() + 1, formatOptions); dictBuffer.position(oldNodeAddress); final int currentFlags = dictBuffer.readUnsignedByte(); dictBuffer.position(oldNodeAddress); dictBuffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG)))); int size = FormatSpec.PTNODE_FLAGS_SIZE; - updateForwardLink(dictBuffer, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions); + updateForwardLink(dictUpdater, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions); size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info }); return size; } @SuppressWarnings("unused") - private static void updateForwardLink(final DictBuffer dictBuffer, + private static void updateForwardLink(final Ver3DictUpdater dictUpdater, final int nodeArrayOriginAddress, final int newNodeArrayAddress, final FormatOptions formatOptions) { - dictBuffer.position(nodeArrayOriginAddress); + final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); + dictUpdater.setPosition(nodeArrayOriginAddress); int jumpCount = 0; while (jumpCount++ < MAX_JUMPS) { - final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); + final int count = dictUpdater.readPtNodeCount(); for (int i = 0; i < count; ++i) { - BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions); + dictUpdater.readPtNode(dictUpdater.getPosition(), formatOptions); } final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) { @@ -208,7 +187,7 @@ public final class DynamicBinaryDictIOUtils { * @param shortcutTargets the shortcut targets for this PtNode. * @param bigrams the bigrams for this PtNode. * @param destination the stream representing the tail of the file. - * @param dictBuffer the DictBuffer representing the (constant-size) body of the file. + * @param dictUpdater the DictUpdater. * @param oldPtNodeArrayOrigin the origin of the old PtNode array this PtNode was a part of. * @param oldPtNodeOrigin the old origin where this PtNode used to be stored. * @param formatOptions format options for this dictionary. @@ -219,7 +198,7 @@ public final class DynamicBinaryDictIOUtils { final int length, final int flags, final int frequency, final int parentAddress, final ArrayList<WeightedString> shortcutTargets, final ArrayList<PendingAttribute> bigrams, final OutputStream destination, - final DictBuffer dictBuffer, final int oldPtNodeArrayOrigin, + final Ver3DictUpdater dictUpdater, final int oldPtNodeArrayOrigin, final int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException { int size = 0; final int newPtNodeOrigin = fileEndAddress + 1; @@ -232,7 +211,7 @@ public final class DynamicBinaryDictIOUtils { flags, writtenCharacters, frequency, parentAddress, fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets, bigrams); - movePtNode(destination, dictBuffer, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin, + movePtNode(destination, dictUpdater, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin, formatOptions); return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE; } @@ -240,7 +219,7 @@ public final class DynamicBinaryDictIOUtils { /** * Insert a word into a binary dictionary. * - * @param dictDecoder the dict decoder. + * @param dictUpdater the dict updater. * @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. @@ -253,17 +232,17 @@ public final class DynamicBinaryDictIOUtils { // TODO: Support batch insertion. // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary. @UsedForTesting - public static void insertWord(final Ver3DictDecoder dictDecoder, + public static void insertWord(final Ver3DictUpdater dictUpdater, 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 DictBuffer dictBuffer = dictDecoder.getDictBuffer(); + final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); if (bigramStrings != null) { for (final WeightedString bigram : bigramStrings) { - int position = dictDecoder.getTerminalPosition(bigram.mWord); + int position = dictUpdater.getTerminalPosition(bigram.mWord); if (position == FormatSpec.NOT_VALID_WORD) { // TODO: figure out what is the correct thing to do here. } else { @@ -278,7 +257,7 @@ public final class DynamicBinaryDictIOUtils { // find the insert position of the word. if (dictBuffer.position() != 0) dictBuffer.position(0); - final FileHeader fileHeader = dictDecoder.readHeader(); + final FileHeader fileHeader = dictUpdater.readHeader(); int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position(); final int[] codePoints = FusionDictionary.getCodePoints(word); @@ -293,7 +272,7 @@ public final class DynamicBinaryDictIOUtils { for (int i = 0; i < ptNodeCount; ++i) { address = dictBuffer.position(); - final PtNodeInfo currentInfo = dictDecoder.readPtNode(address, + final PtNodeInfo currentInfo = dictUpdater.readPtNode(address, fileHeader.mFormatOptions); final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, fileHeader.mFormatOptions); @@ -319,12 +298,12 @@ public final class DynamicBinaryDictIOUtils { false /* isBlackListEntry */, fileHeader.mFormatOptions); int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags, frequency, nodeParentAddress, shortcuts, bigrams, destination, - dictBuffer, nodeOriginAddress, address, fileHeader.mFormatOptions); + dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions); final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p, currentInfo.mCharacters.length); if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictBuffer, currentInfo.mChildrenAddress, + updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, newNodeAddress + written + 1, fileHeader.mFormatOptions); } final PtNodeInfo newInfo2 = new PtNodeInfo( @@ -360,13 +339,13 @@ public final class DynamicBinaryDictIOUtils { fileHeader.mFormatOptions); int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, prefixFlags, -1 /* frequency */, nodeParentAddress, null, null, - destination, dictBuffer, nodeOriginAddress, address, + destination, dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions); final int[] suffixCharacters = Arrays.copyOfRange( currentInfo.mCharacters, p, currentInfo.mCharacters.length); if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictBuffer, currentInfo.mChildrenAddress, + updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, newNodeAddress + written + 1, fileHeader.mFormatOptions); } final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags( @@ -417,7 +396,7 @@ public final class DynamicBinaryDictIOUtils { -1 /* endAddress */, flags, currentInfo.mCharacters, frequency, nodeParentAddress, currentInfo.mChildrenAddress, shortcuts, bigrams); - movePtNode(destination, dictBuffer, newInfo, nodeOriginAddress, address, + movePtNode(destination, dictUpdater, newInfo, nodeOriginAddress, address, fileHeader.mFormatOptions); return; } @@ -436,7 +415,7 @@ public final class DynamicBinaryDictIOUtils { * ab - cd - e */ final int newNodeArrayAddress = dictBuffer.limit(); - updateChildrenAddress(dictBuffer, address, newNodeArrayAddress, + updateChildrenAddress(dictUpdater, address, newNodeArrayAddress, fileHeader.mFormatOptions); final int newNodeAddress = newNodeArrayAddress + 1; final boolean hasMultipleChars = (wordLen - wordPos) > 1; diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index 849bff050..a5516bd41 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -265,8 +265,15 @@ public final class FormatSpec { static final String FREQ_FILE_EXTENSION = ".freq"; // tat = Terminal Address Table static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat"; + static final String BIGRAM_FILE_EXTENSION = ".bigram"; + static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup"; + static final String CONTENT_TABLE_FILE_SUFFIX = "_index"; static final int FREQUENCY_AND_FLAGS_SIZE = 2; static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3; + static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4; + static final int BIGRAM_CONTENT_COUNT = 1; + static final int BIGRAM_FREQ_CONTENT_INDEX = 0; + static final String BIGRAM_FREQ_CONTENT_ID = "_freq"; static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; static final int NO_PARENT_ADDRESS = 0; @@ -331,9 +338,9 @@ public final class FormatSpec { public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE"; public static final String ATTRIBUTE_VALUE_TRUE = "1"; - private static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; - private static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; - private static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; + public static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; + public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; + public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description"; public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, final FormatOptions formatOptions) { diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java index 0b9cf91d2..7592a0c13 100644 --- a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java +++ b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java @@ -17,7 +17,11 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.utils.CollectionUtils; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -34,35 +38,39 @@ public class SparseTable { /** * mLookupTable is indexed by terminal ID, containing exactly one entry for every mBlockSize * terminals. - * It contains at index i = j / mBlockSize the index in mContentsTable where the values for - * terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized integer array. + * It contains at index i = j / mBlockSize the index in each ArrayList in mContentsTables where + * the values for terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized + * integer array. */ private final ArrayList<Integer> mLookupTable; - private final ArrayList<Integer> mContentTable; + private final ArrayList<ArrayList<Integer>> mContentTables; private final int mBlockSize; + private final int mContentTableCount; public static final int NOT_EXIST = -1; + public static final int SIZE_OF_INT_IN_BYTES = 4; @UsedForTesting - public SparseTable(final int initialCapacity, final int blockSize) { + public SparseTable(final int initialCapacity, final int blockSize, + final int contentTableCount) { mBlockSize = blockSize; final int lookupTableSize = initialCapacity / mBlockSize + (initialCapacity % mBlockSize > 0 ? 1 : 0); mLookupTable = new ArrayList<Integer>(Collections.nCopies(lookupTableSize, NOT_EXIST)); - mContentTable = new ArrayList<Integer>(); + mContentTableCount = contentTableCount; + mContentTables = CollectionUtils.newArrayList(); + for (int i = 0; i < mContentTableCount; ++i) { + mContentTables.add(new ArrayList<Integer>()); + } } @UsedForTesting - public SparseTable(final int[] lookupTable, final int[] contentTable, final int blockSize) { + public SparseTable(final ArrayList<Integer> lookupTable, + final ArrayList<ArrayList<Integer>> contentTables, final int blockSize) { mBlockSize = blockSize; - mLookupTable = new ArrayList<Integer>(lookupTable.length); - for (int i = 0; i < lookupTable.length; ++i) { - mLookupTable.add(lookupTable[i]); - } - mContentTable = new ArrayList<Integer>(contentTable.length); - for (int i = 0; i < contentTable.length; ++i) { - mContentTable.add(contentTable[i]); - } + mContentTableCount = contentTables.size(); + mLookupTable = lookupTable; + mContentTables = contentTables; } /** @@ -72,8 +80,8 @@ public class SparseTable { * Otherwise, IndexOutOfBoundsException will be raised. */ @UsedForTesting - private static void convertByteArrayToIntegerArray(final byte[] byteArray, - final ArrayList<Integer> integerArray) { + private static ArrayList<Integer> convertByteArrayToIntegerArray(final byte[] byteArray) { + final ArrayList<Integer> integerArray = new ArrayList<Integer>(byteArray.length / 4); for (int i = 0; i < byteArray.length; i += 4) { int value = 0; for (int j = i; j < i + 4; ++j) { @@ -82,39 +90,43 @@ public class SparseTable { } integerArray.add(value); } + return integerArray; } @UsedForTesting - public SparseTable(final byte[] lookupTable, final byte[] contentTable, final int blockSize) { - mBlockSize = blockSize; - mLookupTable = new ArrayList<Integer>(lookupTable.length / 4); - mContentTable = new ArrayList<Integer>(contentTable.length / 4); - convertByteArrayToIntegerArray(lookupTable, mLookupTable); - convertByteArrayToIntegerArray(contentTable, mContentTable); + public int get(final int contentTableIndex, final int index) { + if (!contains(index)) { + return NOT_EXIST; + } + return mContentTables.get(contentTableIndex).get( + mLookupTable.get(index / mBlockSize) + (index % mBlockSize)); } @UsedForTesting - public int get(final int index) { - if (index < 0 || index / mBlockSize >= mLookupTable.size() - || mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - return NOT_EXIST; + public ArrayList<Integer> getAll(final int index) { + final ArrayList<Integer> ret = CollectionUtils.newArrayList(); + for (int i = 0; i < mContentTableCount; ++i) { + ret.add(get(i, index)); } - return mContentTable.get(mLookupTable.get(index / mBlockSize) + (index % mBlockSize)); + return ret; } @UsedForTesting - public void set(final int index, final int value) { + public void set(final int contentTableIndex, final int index, final int value) { if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - mLookupTable.set(index / mBlockSize, mContentTable.size()); - for (int i = 0; i < mBlockSize; ++i) { - mContentTable.add(NOT_EXIST); + mLookupTable.set(index / mBlockSize, mContentTables.get(contentTableIndex).size()); + for (int i = 0; i < mContentTableCount; ++i) { + for (int j = 0; j < mBlockSize; ++j) { + mContentTables.get(i).add(NOT_EXIST); + } } } - mContentTable.set(mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value); + mContentTables.get(contentTableIndex).set( + mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value); } - public void remove(final int index) { - set(index, NOT_EXIST); + public void remove(final int indexOfContent, final int index) { + set(indexOfContent, index, NOT_EXIST); } @UsedForTesting @@ -124,7 +136,8 @@ public class SparseTable { @UsedForTesting /* package */ int getContentTableSize() { - return mContentTable.size(); + // This class always has at least one content table. + return mContentTables.get(0).size(); } @UsedForTesting @@ -133,18 +146,78 @@ public class SparseTable { } public boolean contains(final int index) { - return get(index) != NOT_EXIST; + if (index < 0 || index / mBlockSize >= mLookupTable.size() + || mLookupTable.get(index / mBlockSize) == NOT_EXIST) { + return false; + } + return true; } @UsedForTesting - public void write(final OutputStream lookupOutStream, final OutputStream contentOutStream) + public void write(final OutputStream lookupOutStream, final OutputStream[] contentOutStreams) throws IOException { + if (contentOutStreams.length != mContentTableCount) { + throw new RuntimeException(contentOutStreams.length + " streams are given, but the" + + " table has " + mContentTableCount + " content tables."); + } for (final int index : mLookupTable) { - BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, 4); + BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, SIZE_OF_INT_IN_BYTES); } - for (final int index : mContentTable) { - BinaryDictEncoderUtils.writeUIntToStream(contentOutStream, index, 4); + for (int i = 0; i < contentOutStreams.length; ++i) { + for (final int data : mContentTables.get(i)) { + BinaryDictEncoderUtils.writeUIntToStream(contentOutStreams[i], data, + SIZE_OF_INT_IN_BYTES); + } + } + } + + @UsedForTesting + public void writeToFiles(final File lookupTableFile, final File[] contentFiles) + throws IOException { + FileOutputStream lookupTableOutStream = null; + final FileOutputStream[] contentTableOutStreams = new FileOutputStream[mContentTableCount]; + try { + lookupTableOutStream = new FileOutputStream(lookupTableFile); + for (int i = 0; i < contentFiles.length; ++i) { + contentTableOutStreams[i] = new FileOutputStream(contentFiles[i]); + } + write(lookupTableOutStream, contentTableOutStreams); + } finally { + if (lookupTableOutStream != null) { + lookupTableOutStream.close(); + } + for (int i = 0; i < contentTableOutStreams.length; ++i) { + if (contentTableOutStreams[i] != null) { + contentTableOutStreams[i].close(); + } + } + } + } + + private static byte[] readFileToByteArray(final File file) throws IOException { + final byte[] contents = new byte[(int) file.length()]; + FileInputStream inStream = null; + try { + inStream = new FileInputStream(file); + inStream.read(contents); + } finally { + if (inStream != null) { + inStream.close(); + } + } + return contents; + } + + @UsedForTesting + public static SparseTable readFromFiles(final File lookupTableFile, final File[] contentFiles, + final int blockSize) throws IOException { + final ArrayList<ArrayList<Integer>> contentTables = + new ArrayList<ArrayList<Integer>>(contentFiles.length); + for (int i = 0; i < contentFiles.length; ++i) { + contentTables.add(convertByteArrayToIntegerArray(readFileToByteArray(contentFiles[i]))); } + return new SparseTable(convertByteArrayToIntegerArray(readFileToByteArray(lookupTableFile)), + contentTables, blockSize); } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java index 848277cd4..b87259c38 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java @@ -53,9 +53,9 @@ public class Ver3DictDecoder extends DictDecoder { } } - private final File mDictionaryBinaryFile; + protected final File mDictionaryBinaryFile; private final DictionaryBufferFactory mBufferFactory; - private DictBuffer mDictBuffer; + protected DictBuffer mDictBuffer; /* package */ Ver3DictDecoder(final File file, final int factoryFlag) { mDictionaryBinaryFile = file; @@ -169,7 +169,8 @@ public class Ver3DictDecoder extends DictDecoder { addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams, addressPointer); if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - MakedictLog.d("too many bigrams in a PtNode."); + throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() + + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); } } else { bigrams = null; @@ -231,4 +232,40 @@ public class Ver3DictDecoder extends DictDecoder { public boolean hasNextPtNodeArray() { return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; } + + @Override + public void skipPtNode(final FormatOptions formatOptions) { + final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); + PtNodeReader.readParentAddress(mDictBuffer, formatOptions); + BinaryDictIOUtils.skipString(mDictBuffer, + (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); + PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); + if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readFrequency(mDictBuffer); + if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) { + final int shortcutsSize = mDictBuffer.readUnsignedShort(); + mDictBuffer.position(mDictBuffer.position() + shortcutsSize + - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); + } + if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) { + int bigramCount = 0; + while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { + final int bigramFlags = mDictBuffer.readUnsignedByte(); + switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { + case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: + mDictBuffer.readUnsignedByte(); + break; + case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: + mDictBuffer.readUnsignedShort(); + break; + case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: + mDictBuffer.readUnsignedInt24(); + break; + } + if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break; + } + if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { + throw new RuntimeException("Too many bigrams in a PtNode."); + } + } + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java index 76f0f4052..d9e19899c 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java @@ -133,12 +133,10 @@ public class Ver3DictEncoder implements DictEncoder { countSize); } - private void writePtNodeFlags(final PtNode ptNode, final int parentAddress, - final FormatOptions formatOptions) { + private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, childrenPos, - formatOptions), + BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), FormatSpec.PTNODE_FLAGS_SIZE); } @@ -244,7 +242,7 @@ public class Ver3DictEncoder implements DictEncoder { @Override public void writePtNode(final PtNode ptNode, final int parentPosition, final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, parentPosition, formatOptions); + writePtNodeFlags(ptNode, formatOptions); writeParentPosition(parentPosition, ptNode, formatOptions); writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); writeFrequency(ptNode.mFrequency); diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java new file mode 100644 index 000000000..fa7ae310a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java @@ -0,0 +1,82 @@ +/* + * 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; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * An implementation of DictUpdater for version 3 binary dictionary. + */ +@UsedForTesting +public class Ver3DictUpdater extends Ver3DictDecoder implements DictUpdater { + private OutputStream mOutStream; + + @UsedForTesting + public Ver3DictUpdater(final File dictFile, final int factoryType) { + // DictUpdater must have an updatable DictBuffer. + super(dictFile, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY) + ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER); + mOutStream = null; + } + + private void openStreamAndBuffer() throws FileNotFoundException, IOException { + super.openDictBuffer(); + mOutStream = new FileOutputStream(mDictionaryBinaryFile, true /* append */); + } + + private void close() throws IOException { + if (mOutStream != null) { + mOutStream.close(); + mOutStream = null; + } + } + + @Override @UsedForTesting + public void deleteWord(final String word) throws IOException, UnsupportedFormatException { + if (mOutStream == null) openStreamAndBuffer(); + mDictBuffer.position(0); + super.readHeader(); + final int wordPos = getTerminalPosition(word); + if (wordPos != FormatSpec.NOT_VALID_WORD) { + mDictBuffer.position(wordPos); + final int flags = mDictBuffer.readUnsignedByte(); + mDictBuffer.position(wordPos); + mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags)); + } + close(); + } + + @Override @UsedForTesting + public void insertWord(final String word, final int frequency, + final ArrayList<WeightedString> bigramStrings, + final ArrayList<WeightedString> shortcuts, + final boolean isNotAWord, final boolean isBlackListEntry) + throws IOException, UnsupportedFormatException { + if (mOutStream == null) openStreamAndBuffer(); + DynamicBinaryDictIOUtils.insertWord(this, mOutStream, word, frequency, bigramStrings, + shortcuts, isNotAWord, isBlackListEntry); + close(); + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java index 4c8ff8ea4..5089687da 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java @@ -42,12 +42,15 @@ public class Ver4DictDecoder extends DictDecoder { private static final int FILETYPE_TRIE = 1; private static final int FILETYPE_FREQUENCY = 2; private static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3; + private static final int FILETYPE_BIGRAM_FREQ = 4; private final File mDictDirectory; private final DictionaryBufferFactory mBufferFactory; private DictBuffer mDictBuffer; private DictBuffer mFrequencyBuffer; private DictBuffer mTerminalAddressTableBuffer; + private DictBuffer mBigramBuffer; + private SparseTable mBigramAddressTable; @UsedForTesting /* package */ Ver4DictDecoder(final File dictDirectory, final int factoryFlag) { @@ -82,6 +85,10 @@ public class Ver4DictDecoder extends DictDecoder { } else if (fileType == FILETYPE_TERMINAL_ADDRESS_TABLE) { return new File(mDictDirectory, mDictDirectory.getName() + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); + } else if (fileType == FILETYPE_BIGRAM_FREQ) { + return new File(mDictDirectory, + mDictDirectory.getName() + FormatSpec.BIGRAM_FILE_EXTENSION + + FormatSpec.BIGRAM_FREQ_CONTENT_ID); } else { throw new RuntimeException("Unsupported kind of file : " + fileType); } @@ -89,11 +96,12 @@ public class Ver4DictDecoder extends DictDecoder { @Override public void openDictBuffer() throws FileNotFoundException, IOException { - final String filename = mDictDirectory.getName(); mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE)); mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY)); mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer( getFile(FILETYPE_TERMINAL_ADDRESS_TABLE)); + mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM_FREQ)); + loadBigramAddressSparseTable(); } @Override @@ -118,6 +126,16 @@ public class Ver4DictDecoder extends DictDecoder { return header; } + private void loadBigramAddressSparseTable() throws IOException { + final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName() + + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); + final File freqsFile = new File(mDictDirectory, mDictDirectory.getName() + + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + + FormatSpec.BIGRAM_FREQ_CONTENT_ID); + mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, new File[] { freqsFile }, + FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE); + } + protected static class PtNodeReader extends DictDecoder.PtNodeReader { protected static int readFrequency(final DictBuffer frequencyBuffer, final int terminalId) { frequencyBuffer.position(terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE + 1); @@ -191,10 +209,24 @@ public class Ver4DictDecoder extends DictDecoder { final ArrayList<PendingAttribute> bigrams; if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { bigrams = new ArrayList<PendingAttribute>(); - addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams, - addressPointer); + final int posOfBigrams = mBigramAddressTable.get(0 /* contentTableIndex */, terminalId); + mBigramBuffer.position(posOfBigrams); + while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { + // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE, + // remaining bigram entries are ignored. + final int bigramFlags = mBigramBuffer.readUnsignedByte(); + final int targetTerminalId = mBigramBuffer.readUnsignedInt24(); + mTerminalAddressTableBuffer.position( + targetTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE); + final int targetAddress = mTerminalAddressTableBuffer.readUnsignedInt24(); + bigrams.add(new PendingAttribute( + bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY, + targetAddress)); + if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; + } if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - MakedictLog.d("too many bigrams in a node."); + throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() + + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); } } else { bigrams = null; @@ -263,4 +295,14 @@ public class Ver4DictDecoder extends DictDecoder { public boolean hasNextPtNodeArray() { return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; } + + @Override + public void skipPtNode(final FormatOptions formatOptions) { + final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); + PtNodeReader.readParentAddress(mDictBuffer, formatOptions); + BinaryDictIOUtils.skipString(mDictBuffer, + (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); + if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readTerminalId(mDictBuffer); + PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java index 4fb89671f..b38c33019 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java @@ -46,21 +46,121 @@ public class Ver4DictEncoder implements DictEncoder { private OutputStream mTrieOutStream; private OutputStream mFreqOutStream; private OutputStream mTerminalAddressTableOutStream; + private File mDictDir; + private String mBaseFilename; + private BigramContentWriter mBigramWriter; @UsedForTesting public Ver4DictEncoder(final File dictPlacedDir) { mDictPlacedDir = dictPlacedDir; } + private interface SparseTableContentWriterInterface { + public void write(final OutputStream outStream) throws IOException; + } + + private static class SparseTableContentWriter { + private final int mContentCount; + private final SparseTable mSparseTable; + private final File mLookupTableFile; + protected final File mBaseDir; + private final File[] mAddressTableFiles; + private final File[] mContentFiles; + protected final OutputStream[] mContentOutStreams; + + public SparseTableContentWriter(final String name, final int contentCount, + final int initialCapacity, final int blockSize, final File baseDir, + final String[] contentFilenames, final String[] contentIds) { + if (contentFilenames.length != contentIds.length) { + throw new RuntimeException("The length of contentFilenames and the length of" + + " contentIds are different " + contentFilenames.length + ", " + + contentIds.length); + } + mContentCount = contentCount; + mSparseTable = new SparseTable(initialCapacity, blockSize, contentCount); + mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); + mAddressTableFiles = new File[mContentCount]; + mContentFiles = new File[mContentCount]; + mBaseDir = baseDir; + for (int i = 0; i < mContentCount; ++i) { + mAddressTableFiles[i] = new File(mBaseDir, + name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]); + mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]); + } + mContentOutStreams = new OutputStream[mContentCount]; + } + + public void openStreams() throws FileNotFoundException { + for (int i = 0; i < mContentCount; ++i) { + mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]); + } + } + + protected void write(final int contentIndex, final int index, + final SparseTableContentWriterInterface writer) throws IOException { + mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length()); + writer.write(mContentOutStreams[contentIndex]); + mContentOutStreams[contentIndex].flush(); + } + + public void closeStreams() throws IOException { + mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles); + for (int i = 0; i < mContentCount; ++i) { + mContentOutStreams[i].close(); + } + } + } + + private static class BigramContentWriter extends SparseTableContentWriter { + + public BigramContentWriter(final String name, final int initialCapacity, + final File baseDir) { + super(name + FormatSpec.BIGRAM_FILE_EXTENSION, FormatSpec.BIGRAM_CONTENT_COUNT, + initialCapacity, FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir, + new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION }, + new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID }); + } + + public void writeBigramsForOneWord(final int terminalId, + final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) + throws IOException { + write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId, + new SparseTableContentWriterInterface() { + @Override + public void write(final OutputStream outStream) throws IOException { + writeBigramsForOneWordInternal(outStream, bigramIterator, dict); + } + }); + } + + private void writeBigramsForOneWordInternal(final OutputStream outStream, + final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) + throws IOException { + while (bigramIterator.hasNext()) { + final WeightedString bigram = bigramIterator.next(); + final PtNode target = + FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); + final int unigramFrequencyForThisWord = target.mFrequency; + final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags( + bigramIterator.hasNext(), 0, bigram.mFrequency, + unigramFrequencyForThisWord, bigram.mWord); + BinaryDictEncoderUtils.writeUIntToStream(outStream, bigramFlags, + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); + BinaryDictEncoderUtils.writeUIntToStream(outStream, target.mTerminalId, + FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE); + } + } + } + private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions) throws FileNotFoundException, IOException { final FileHeader header = new FileHeader(0, dictOptions, formatOptions); - final String filename = header.getId() + "." + header.getVersion(); - final File mDictDir = new File(mDictPlacedDir, filename); - final File trieFile = new File(mDictDir, filename + FormatSpec.TRIE_FILE_EXTENSION); - final File freqFile = new File(mDictDir, filename + FormatSpec.FREQ_FILE_EXTENSION); + mBaseFilename = header.getId() + "." + header.getVersion(); + mDictDir = new File(mDictPlacedDir, mBaseFilename); + final File trieFile = new File(mDictDir, mBaseFilename + FormatSpec.TRIE_FILE_EXTENSION); + final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION); final File terminalAddressTableFile = new File(mDictDir, - filename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); + mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); if (!mDictDir.isDirectory()) { if (mDictDir.exists()) mDictDir.delete(); mDictDir.mkdirs(); @@ -123,6 +223,8 @@ public class Ver4DictEncoder implements DictEncoder { if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); writeTerminalData(flatNodes, terminalCount); + mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir); + writeBigrams(flatNodes, dict); final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; @@ -165,12 +267,10 @@ public class Ver4DictEncoder implements DictEncoder { countSize); } - private void writePtNodeFlags(final PtNode ptNode, final int parentAddress, - final FormatOptions formatOptions) { + private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mTriePos, childrenPos, - formatOptions), + BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), FormatSpec.PTNODE_FLAGS_SIZE); } @@ -215,8 +315,7 @@ public class Ver4DictEncoder implements DictEncoder { while (shortcutIterator.hasNext()) { final WeightedString target = shortcutIterator.next(); final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), - target.mFrequency); + shortcutIterator.hasNext(), target.mFrequency); mTrieBuf[mTriePos++] = (byte)shortcutFlags; final int shortcutShift = CharEncoding.writeString(mTrieBuf, mTriePos, target.mWord); @@ -230,24 +329,18 @@ public class Ver4DictEncoder implements DictEncoder { shortcutByteSize, FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); } - private void writeBigrams(ArrayList<WeightedString> bigrams, FusionDictionary dict) { - if (bigrams == null) return; - - final Iterator<WeightedString> bigramIterator = bigrams.iterator(); - while (bigramIterator.hasNext()) { - final WeightedString bigram = bigramIterator.next(); - final PtNode target = - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); - final int addressOfBigram = target.mCachedAddressAfterUpdate; - final int unigramFrequencyForThisWord = target.mFrequency; - final int offset = addressOfBigram - - (mTriePos + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), - offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord); - mTrieBuf[mTriePos++] = (byte) bigramFlags; - mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf, - mTriePos, Math.abs(offset)); + private void writeBigrams(final ArrayList<PtNodeArray> flatNodes, final FusionDictionary dict) + throws IOException { + mBigramWriter.openStreams(); + for (final PtNodeArray nodeArray : flatNodes) { + for (final PtNode ptNode : nodeArray.mData) { + if (ptNode.mBigrams != null) { + mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId, + ptNode.mBigrams.iterator(), dict); + } + } } + mBigramWriter.closeStreams(); } @Override @@ -259,7 +352,7 @@ public class Ver4DictEncoder implements DictEncoder { @Override public void writePtNode(final PtNode ptNode, final int parentPosition, final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, parentPosition, formatOptions); + writePtNodeFlags(ptNode, formatOptions); writeParentPosition(parentPosition, ptNode, formatOptions); writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); if (ptNode.isTerminal()) { @@ -267,7 +360,6 @@ public class Ver4DictEncoder implements DictEncoder { } writeChildrenPosition(ptNode, formatOptions); writeShortcuts(ptNode.mShortcutTargets); - writeBigrams(ptNode.mBigrams, dict); } private void writeTerminalData(final ArrayList<PtNodeArray> flatNodes, diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java index 66517a800..c8b62b6c8 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java @@ -22,6 +22,7 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.makedict.DictDecoder; @@ -50,6 +51,9 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB /** Any pair being typed or picked */ public static final int FREQUENCY_FOR_TYPED = 2; + public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED; + public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY; + /** Locale for which this user history dictionary is storing words */ private final String mLocale; @@ -94,6 +98,8 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE, FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName); + attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale); return attributeMap; } @@ -117,27 +123,29 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB } /** - * Pair will be added to the personalization prediction dictionary. + * Pair will be added to the decaying dictionary. * * The first word may be null. That means we don't know the context, in other words, * it's only a unigram. The first word may also be an empty string : this means start * context, as in beginning of a sentence for example. * The second word may not be null (a NullPointerException would be thrown). */ - public void addToPersonalizationPredictionDictionary( - final String word0, final String word1, final boolean isValid) { + public void addToDictionary(final String word0, final String word1, final boolean isValid) { if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH || (word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) { return; } - addWordDynamically(word1, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED, + final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ? + (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) : + FREQUENCY_FOR_TYPED; + addWordDynamically(word1, null /* the "shortcut" parameter is null */, frequency, false /* isNotAWord */); // Do not insert a word as a bigram of itself if (word1.equals(word0)) { return; } if (null != word0) { - addBigramDynamically(word0, word1, FREQUENCY_FOR_TYPED, isValid); + addBigramDynamically(word0, word1, frequency, isValid); } } @@ -222,10 +230,15 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB mSessions.remove(session); } + @UsedForTesting public void clearAndFlushDictionary() { // Clear the node structure on memory clear(); // Then flush the cleared state of the dictionary on disk. asyncFlashAllBinaryDictionary(); } + + /* package */ void decayIfNeeded() { + runGCIfRequired(false /* mindsBlockByGC */); + } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java new file mode 100644 index 000000000..e9ca662e7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java @@ -0,0 +1,66 @@ +/* + * 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.personalization; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.concurrent.TimeUnit; + +/** + * Broadcast receiver for periodically updating decaying dictionaries. + */ +public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { + /** + * The root domain for the personalization. + */ + private static final String PERSONALIZATION_DOMAIN = + "com.android.inputmethod.latin.personalization"; + + /** + * The action of the intent to tell the time to decay dictionaries. + */ + private static final String DICTIONARY_DECAY_INTENT_ACTION = + PERSONALIZATION_DOMAIN + ".DICT_DECAY"; + + /** + * Interval to update for decaying dictionaries. + */ + private static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); + + public static void setUpIntervalAlarmForDictionaryDecaying(Context context) { + AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + final Intent updateIntent = new Intent(DICTIONARY_DECAY_INTENT_ACTION); + updateIntent.setClass(context, DictionaryDecayBroadcastReciever.class); + final long alarmTime = System.currentTimeMillis() + DICTIONARY_DECAY_INTERVAL; + final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 /* requestCode */, + updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); + if (null != alarmManager) alarmManager.setInexactRepeating(AlarmManager.RTC, + alarmTime, DICTIONARY_DECAY_INTERVAL, pendingIntent); + } + + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) { + PersonalizationHelper.tryDecayingAllOpeningUserHistoryDictionary(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java index 305088536..039b25337 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java +++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java @@ -36,6 +36,7 @@ import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.Forge import java.io.IOException; import java.util.ArrayList; +import java.util.Map; // Currently this class is used to implement dynamic prodiction dictionary. // TODO: Move to native code. @@ -113,8 +114,8 @@ public class DynamicPersonalizationDictionaryWriter extends AbstractDictionaryWr } @Override - protected void writeDictionary(final DictEncoder dictEncoder) - throws IOException, UnsupportedFormatException { + protected void writeDictionary(final DictEncoder dictEncoder, + final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { UserHistoryDictIOUtils.writeDictionary(dictEncoder, new FrequencyProvider(mBigramList, mExpandableDictionary, mMaxHistoryBigrams), mBigramList, FORMAT_OPTIONS); diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java index c616a296c..a86f6e584 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java @@ -110,7 +110,7 @@ public abstract class PersonalizationDictionaryUpdateSession { if (dictionary == null) { return; } - dictionary.addToPersonalizationPredictionDictionary(word0, word1, isValid); + dictionary.addToDictionary(word0, word1, isValid); } // Bulk import @@ -122,8 +122,7 @@ public abstract class PersonalizationDictionaryUpdateSession { return; } for (final PersonalizationLanguageModelParam lmParam : lmParams) { - dictionary.addToPersonalizationPredictionDictionary( - lmParam.mWord0, lmParam.mWord1, lmParam.mIsValid); + dictionary.addToDictionary(lmParam.mWord0, lmParam.mWord1, lmParam.mIsValid); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java index 5f702ee3f..221ddeeba 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java @@ -29,8 +29,7 @@ import java.util.concurrent.ConcurrentHashMap; public class PersonalizationHelper { private static final String TAG = PersonalizationHelper.class.getSimpleName(); private static final boolean DEBUG = false; - - private static final ConcurrentHashMap<String, SoftReference<UserHistoryPredictionDictionary>> + private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap(); private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>> @@ -41,29 +40,39 @@ public class PersonalizationHelper { sLangPersonalizationPredictionDictCache = CollectionUtils.newConcurrentHashMap(); - public static UserHistoryPredictionDictionary getUserHistoryPredictionDictionary( + public static UserHistoryDictionary getUserHistoryDictionary( final Context context, final String locale, final SharedPreferences sp) { synchronized (sLangUserHistoryDictCache) { if (sLangUserHistoryDictCache.containsKey(locale)) { - final SoftReference<UserHistoryPredictionDictionary> ref = + final SoftReference<UserHistoryDictionary> ref = sLangUserHistoryDictCache.get(locale); - final UserHistoryPredictionDictionary dict = ref == null ? null : ref.get(); + final UserHistoryDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { - Log.w(TAG, "Use cached UserHistoryPredictionDictionary for " + locale); + Log.w(TAG, "Use cached UserHistoryDictionary for " + locale); } dict.reloadDictionaryIfRequired(); return dict; } } - final UserHistoryPredictionDictionary dict = - new UserHistoryPredictionDictionary(context, locale, sp); - sLangUserHistoryDictCache.put( - locale, new SoftReference<UserHistoryPredictionDictionary>(dict)); + final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale, sp); + sLangUserHistoryDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict)); return dict; } } + public static void tryDecayingAllOpeningUserHistoryDictionary() { + for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry + : sLangUserHistoryDictCache.entrySet()) { + if (entry.getValue() != null) { + final UserHistoryDictionary dict = entry.getValue().get(); + if (dict != null) { + dict.decayIfNeeded(); + } + } + } + } + public static void registerPersonalizationDictionaryUpdateSession(final Context context, final PersonalizationDictionaryUpdateSession session, String locale) { final PersonalizationPredictionDictionary predictionDictionary = diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index 38e308a4e..a60226d7e 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -26,10 +26,10 @@ import android.content.SharedPreferences; * Locally gathers stats about the words user types and various other signals like auto-correction * cancellation or manual picks. This allows the keyboard to adapt to the typist over time. */ -public class UserHistoryPredictionDictionary extends DecayingExpandableBinaryDictionaryBase { +public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { /* package for tests */ static final String NAME = - UserHistoryPredictionDictionary.class.getSimpleName(); - /* package */ UserHistoryPredictionDictionary(final Context context, final String locale, + UserHistoryDictionary.class.getSimpleName(); + /* package */ UserHistoryDictionary(final Context context, final String locale, final SharedPreferences sp) { super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, getDictionaryFileName(locale)); } diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java index b499c26b6..ef6ab2a38 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java @@ -38,4 +38,10 @@ public final class DebugSettingsActivity extends PreferenceActivity { super.onCreate(savedInstanceState); setTitle(R.string.english_ime_debug_settings); } + + // TODO: Uncomment the override annotation once we start using SDK version 19. + // @Override + public boolean isValidFragment(String fragmentName) { + return fragmentName.equals(DEFAULT_FRAGMENT); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java index 6c3818651..ad68f8c37 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java @@ -32,4 +32,10 @@ public final class SettingsActivity extends PreferenceActivity { intent.putExtra(EXTRA_NO_HEADERS, true); return intent; } + + // TODO: Uncomment the override annotation once we start using SDK version 19. + // @Override + public boolean isValidFragment(String fragmentName) { + return fragmentName.equals(DEFAULT_FRAGMENT); + } } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java index 119ca4755..aba563746 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java @@ -24,6 +24,8 @@ import android.preference.PreferenceActivity; * Spell checker preference screen. */ public final class SpellCheckerSettingsActivity extends PreferenceActivity { + private static final String DEFAULT_FRAGMENT = SpellCheckerSettingsFragment.class.getName(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -32,8 +34,14 @@ public final class SpellCheckerSettingsActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent modIntent = new Intent(super.getIntent()); - modIntent.putExtra(EXTRA_SHOW_FRAGMENT, SpellCheckerSettingsFragment.class.getName()); + modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); modIntent.putExtra(EXTRA_NO_HEADERS, true); return modIntent; } + + // TODO: Uncomment the override annotation once we start using SDK version 19. + // @Override + public boolean isValidFragment(String fragmentName) { + return fragmentName.equals(DEFAULT_FRAGMENT); + } } diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java index 44b201642..ff332cdee 100644 --- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java @@ -61,10 +61,8 @@ public final class AdditionalSubtypeUtils { StringUtils.appendToCommaSplittableTextIfNotExists( IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue); final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName); - return new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, - localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue - + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE - + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, false, false); + return buildInputMethodSubtype( + nameId, localeString, layoutExtraValue, additionalSubtypeExtraValue); } public static String getPrefSubtype(final InputMethodSubtype subtype) { @@ -137,4 +135,27 @@ public final class AdditionalSubtypeUtils { } return sb.toString(); } + + private static InputMethodSubtype buildInputMethodSubtype(int nameId, String localeString, + String layoutExtraValue, String additionalSubtypeExtraValue) { + // CAVEAT! If you want to change subtypeId after changing the extra values, + // you must change "getInputMethodSubtypeId". But it will remove the additional keyboard + // from the current users. So, you should be really careful to change it. + final int subtypeId = getInputMethodSubtypeId(nameId, localeString, layoutExtraValue, + additionalSubtypeExtraValue); + // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19. + return new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, + localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue + + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, false, false, + subtypeId); + } + + private static int getInputMethodSubtypeId(int nameId, String localeString, + String layoutExtraValue, String additionalSubtypeExtraValue) { + // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19. + return (new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, + localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue, + false, false)).hashCode(); + } } |