aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java12
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java2
-rw-r--r--java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java20
-rw-r--r--java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java370
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java164
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyDetector.java7
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java45
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java6
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java16
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java13
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java47
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java47
-rw-r--r--java/src/com/android/inputmethod/keyboard/MainKeyboardView.java36
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java11
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java50
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java34
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java25
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java352
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java8
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java137
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java161
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java156
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java157
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java212
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java66
-rw-r--r--java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java24
-rw-r--r--java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java12
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java14
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java34
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java6
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java64
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java6
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryWriter.java15
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java310
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java106
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java547
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java1
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java27
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java68
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java25
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java5
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java5
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java3
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java4
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java634
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java1007
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java860
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java1838
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictDecoder.java186
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictEncoder.java29
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java502
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FormatSpec.java202
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java351
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java (renamed from java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java)6
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java350
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java68
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java159
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java325
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java32
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java37
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java74
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java (renamed from java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryHelper.java)53
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java9
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java5
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java11
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java4
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettings.java2
-rw-r--r--java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java3
-rw-r--r--java/src/com/android/inputmethod/latin/settings/Settings.java20
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsFragment.java4
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsValues.java68
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java14
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java3
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java24
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java (renamed from java/src/com/android/inputmethod/latin/utils/ByteArrayWrapper.java)6
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CollectionUtils.java5
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java10
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StringUtils.java82
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java1
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java40
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java12
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java4
-rw-r--r--java/src/com/android/inputmethod/research/JsonUtils.java12
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java2
91 files changed, 6492 insertions, 4089 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 77f323440..7639432aa 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -157,7 +157,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
// Add the virtual children of the root View.
final Keyboard keyboard = mKeyboardView.getKeyboard();
- final Key[] keys = keyboard.mKeys;
+ final Key[] keys = keyboard.getKeys();
for (Key key : keys) {
final int childVirtualViewId = generateVirtualViewIdForKey(key);
rootInfo.addChild(mKeyboardView, childVirtualViewId);
@@ -172,7 +172,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
return null;
}
final String keyDescription = getKeyDescription(key);
- final Rect boundsInParent = key.mHitBox;
+ final Rect boundsInParent = key.getHitBox();
// Calculate the key's in-screen bounds.
mTempBoundsInScreen.set(boundsInParent);
@@ -208,8 +208,8 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
* @param key The key to press.
*/
void simulateKeyPress(final Key key) {
- final int x = key.mHitBox.centerX();
- final int y = key.mHitBox.centerY();
+ final int x = key.getHitBox().centerX();
+ final int y = key.getHitBox().centerY();
final long downTime = SystemClock.uptimeMillis();
final MotionEvent downEvent = MotionEvent.obtain(
downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
@@ -300,7 +300,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
}
mVirtualViewIdToKey.clear();
- final Key[] keys = keyboard.mKeys;
+ final Key[] keys = keyboard.getKeys();
for (Key key : keys) {
final int virtualViewId = generateVirtualViewIdForKey(key);
mVirtualViewIdToKey.put(virtualViewId, key);
@@ -325,6 +325,6 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
// The key x- and y-coordinates are stable between layout changes.
// Generate an identifier by bit-shifting the x-coordinate to the
// left-half of the integer and OR'ing with the y-coordinate.
- return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY);
+ return ((0xFFFF & key.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY());
}
}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 73896dfd3..b3bb767af 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -357,7 +357,6 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp
break;
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
- case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
text = context.getText(R.string.spoken_description_shiftmode_on);
break;
default:
@@ -389,7 +388,6 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp
resId = R.string.spoken_description_mode_alpha;
break;
case KeyboardId.ELEMENT_SYMBOLS:
- case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_mode_symbol;
break;
case KeyboardId.ELEMENT_PHONE:
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 41f5b9a24..085ca93d5 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -97,7 +97,7 @@ public final class KeyCodeDescriptionMapper {
*/
public String getDescriptionForKey(final Context context, final Keyboard keyboard,
final Key key, final boolean shouldObscure) {
- final int code = key.mCode;
+ final int code = key.getCode();
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
@@ -116,8 +116,8 @@ public final class KeyCodeDescriptionMapper {
return getDescriptionForActionKey(context, keyboard, key);
}
- if (!TextUtils.isEmpty(key.mLabel)) {
- final String label = key.mLabel.toString().trim();
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ final String label = key.getLabel().trim();
// First, attempt to map the label to a pre-defined description.
if (mKeyLabelMap.containsKey(label)) {
@@ -126,7 +126,7 @@ public final class KeyCodeDescriptionMapper {
}
// Just attempt to speak the description.
- if (key.mCode != Constants.CODE_UNSPECIFIED) {
+ if (key.getCode() != Constants.CODE_UNSPECIFIED) {
return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
}
return null;
@@ -156,7 +156,6 @@ public final class KeyCodeDescriptionMapper {
resId = R.string.spoken_description_to_symbol;
break;
case KeyboardId.ELEMENT_SYMBOLS:
- case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_to_alpha;
break;
case KeyboardId.ELEMENT_PHONE:
@@ -191,7 +190,6 @@ public final class KeyCodeDescriptionMapper {
break;
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
- case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
resId = R.string.spoken_description_shift_shifted;
break;
default:
@@ -215,8 +213,8 @@ public final class KeyCodeDescriptionMapper {
final int resId;
// Always use the label, if available.
- if (!TextUtils.isEmpty(key.mLabel)) {
- return key.mLabel.toString().trim();
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ return key.getLabel().trim();
}
// Otherwise, use the action ID.
@@ -267,7 +265,7 @@ public final class KeyCodeDescriptionMapper {
*/
private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard,
final Key key, final boolean shouldObscure) {
- final int code = key.mCode;
+ final int code = key.getCode();
// If the key description should be obscured, now is the time to do it.
final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code);
@@ -280,8 +278,8 @@ public final class KeyCodeDescriptionMapper {
if (isDefinedNonCtrl) {
return Character.toString((char) code);
}
- if (!TextUtils.isEmpty(key.mLabel)) {
- return key.mLabel;
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ return key.getLabel();
}
return context.getString(R.string.spoken_description_unknown, code);
}
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
new file mode 100644
index 000000000..25ff8d0ce
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
@@ -0,0 +1,370 @@
+/*
+ * 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.keyboard;
+
+import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TabHost;
+import android.widget.TabHost.OnTabChangeListener;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.internal.RecentsKeyboard;
+import com.android.inputmethod.keyboard.internal.ScrollKeyboardView;
+import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+
+import java.util.HashMap;
+
+/**
+ * View class to implement Emoji keyboards.
+ * The Emoji keyboard consists of group of views {@link R.layout#emoji_keyboard_view}.
+ * <ol>
+ * <li> Emoji category tabs.
+ * <li> Delete button.
+ * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
+ * <li> Back to main keyboard button and enter button.
+ * </ol>
+ * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
+ */
+public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener,
+ ViewPager.OnPageChangeListener, View.OnClickListener,
+ ScrollKeyboardView.OnKeyClickListener {
+ private final int mKeyBackgroundId;
+ private final ColorStateList mTabLabelColor;
+ private final EmojiKeyboardAdapter mEmojiKeyboardAdapter;
+
+ private TabHost mTabHost;
+ private ViewPager mEmojiPager;
+
+ private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
+
+ private int mCurrentCategory = CATEGORY_UNSPECIFIED;
+ private static final int CATEGORY_UNSPECIFIED = -1;
+ private static final int CATEGORY_RECENTS = 0;
+ private static final int CATEGORY_PEOPLE = 1;
+ private static final int CATEGORY_OBJECTS = 2;
+ private static final int CATEGORY_NATURE = 3;
+ private static final int CATEGORY_PLACES = 4;
+ private static final int CATEGORY_SYMBOLS = 5;
+ private static final int CATEGORY_EMOTICONS = 6;
+ private static final HashMap<String, Integer> sCategoryNameToIdMap =
+ CollectionUtils.newHashMap();
+ private static final String[] sCategoryName = {
+ "recents", "people", "objects", "nature", "places", "symbols", "emoticons"
+ };
+ private static final int[] sCategoryIcon = new int[] {
+ R.drawable.ic_emoji_recent_light,
+ R.drawable.ic_emoji_people_light,
+ R.drawable.ic_emoji_objects_light,
+ R.drawable.ic_emoji_nature_light,
+ R.drawable.ic_emoji_places_light,
+ R.drawable.ic_emoji_symbols_light,
+ 0
+ };
+ private static final String[] sCategoryLabel = {
+ null, null, null, null, null, null,
+ ":-)"
+ };
+ private static final int[] sCategoryElementId = {
+ KeyboardId.ELEMENT_EMOJI_RECENTS,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY1,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY2,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY3,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY4,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY5,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY6,
+ };
+
+ public EmojiKeyboardView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.emojiKeyboardViewStyle);
+ }
+
+ public EmojiKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+ mKeyBackgroundId = keyboardViewAttr.getResourceId(
+ R.styleable.KeyboardView_keyBackground, 0);
+ keyboardViewAttr.recycle();
+ final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView);
+ mTabLabelColor = emojiKeyboardViewAttr.getColorStateList(
+ R.styleable.EmojiKeyboardView_emojiTabLabelColor);
+ emojiKeyboardViewAttr.recycle();
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+ context, null /* editorInfo */);
+ final Resources res = context.getResources();
+ builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
+ // TODO: Make Keyboard height variable.
+ builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
+ (int)(ResourceUtils.getDefaultKeyboardHeight(res)
+ - res.getDimension(R.dimen.suggestions_strip_height)));
+ builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */);
+ final KeyboardLayoutSet layoutSet = builder.build();
+ mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(layoutSet, this);
+ // TODO: Save/restore recent keys from/to preferences.
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final Resources res = getContext().getResources();
+ // The main keyboard expands to the entire this {@link KeyboardView}.
+ final int width = ResourceUtils.getDefaultKeyboardWidth(res)
+ + getPaddingLeft() + getPaddingRight();
+ final int height = ResourceUtils.getDefaultKeyboardHeight(res)
+ + res.getDimensionPixelSize(R.dimen.suggestions_strip_height)
+ + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
+ }
+
+ private void addTab(final TabHost host, final int category) {
+ final String tabId = sCategoryName[category];
+ sCategoryNameToIdMap.put(tabId, category);
+ final TabHost.TabSpec tspec = host.newTabSpec(tabId);
+ tspec.setContent(R.id.emoji_keyboard_dummy);
+ if (sCategoryIcon[category] != 0) {
+ final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
+ R.layout.emoji_keyboard_tab_icon, null);
+ iconView.setImageResource(sCategoryIcon[category]);
+ tspec.setIndicator(iconView);
+ }
+ if (sCategoryLabel[category] != null) {
+ final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate(
+ R.layout.emoji_keyboard_tab_label, null);
+ textView.setText(sCategoryLabel[category]);
+ textView.setTextColor(mTabLabelColor);
+ textView.setBackgroundResource(mKeyBackgroundId);
+ tspec.setIndicator(textView);
+ }
+ host.addTab(tspec);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
+ mTabHost.setup();
+ addTab(mTabHost, CATEGORY_RECENTS);
+ addTab(mTabHost, CATEGORY_PEOPLE);
+ addTab(mTabHost, CATEGORY_OBJECTS);
+ addTab(mTabHost, CATEGORY_NATURE);
+ addTab(mTabHost, CATEGORY_PLACES);
+ addTab(mTabHost, CATEGORY_SYMBOLS);
+ addTab(mTabHost, CATEGORY_EMOTICONS);
+ mTabHost.setOnTabChangedListener(this);
+ mTabHost.getTabWidget().setStripEnabled(true);
+
+ mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
+ mEmojiPager.setAdapter(mEmojiKeyboardAdapter);
+ mEmojiPager.setOnPageChangeListener(this);
+ mEmojiPager.setOffscreenPageLimit(0);
+ final ViewGroup.LayoutParams lp = mEmojiPager.getLayoutParams();
+ final Resources res = getResources();
+ lp.height = ResourceUtils.getDefaultKeyboardHeight(res)
+ - res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
+ mEmojiPager.setLayoutParams(lp);
+
+ // TODO: Record current category.
+ final int category = CATEGORY_PEOPLE;
+ setCurrentCategory(category, true /* force */);
+
+ // TODO: Implement auto repeat, using View.OnTouchListener?
+ final View deleteKey = findViewById(R.id.emoji_keyboard_delete);
+ deleteKey.setBackgroundResource(mKeyBackgroundId);
+ deleteKey.setTag(Constants.CODE_DELETE);
+ deleteKey.setOnClickListener(this);
+ final View alphabetKey = findViewById(R.id.emoji_keyboard_alphabet);
+ alphabetKey.setBackgroundResource(mKeyBackgroundId);
+ alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
+ alphabetKey.setOnClickListener(this);
+ final View sendKey = findViewById(R.id.emoji_keyboard_send);
+ sendKey.setBackgroundResource(mKeyBackgroundId);
+ sendKey.setTag(Constants.CODE_ENTER);
+ sendKey.setOnClickListener(this);
+ }
+
+ @Override
+ public void onTabChanged(final String tabId) {
+ final int category = sCategoryNameToIdMap.get(tabId);
+ setCurrentCategory(category, false /* force */);
+ }
+
+
+ @Override
+ public void onPageSelected(final int position) {
+ setCurrentCategory(position, false /* force */);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(final int state) {
+ // Ignore this message. Only want the actual page selected.
+ }
+
+ @Override
+ public void onPageScrolled(final int position, final float positionOffset,
+ final int positionOffsetPixels) {
+ // Ignore this message. Only want the actual page selected.
+ }
+
+ @Override
+ public void onClick(final View v) {
+ if (v.getTag() instanceof Integer) {
+ final int code = (Integer)v.getTag();
+ registerCode(code);
+ return;
+ }
+ }
+
+ private void registerCode(final int code) {
+ mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */);
+ mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE);
+ mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
+ }
+
+ @Override
+ public void onKeyClick(final Key key) {
+ mEmojiKeyboardAdapter.addRecentKey(key);
+ final int code = key.getCode();
+ if (code == Constants.CODE_OUTPUT_TEXT) {
+ mKeyboardActionListener.onTextInput(key.getOutputText());
+ return;
+ }
+ registerCode(code);
+ }
+
+ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+ // TODO:
+ }
+
+ public void setKeyboardActionListener(final KeyboardActionListener listener) {
+ mKeyboardActionListener = listener;
+ }
+
+ private void setCurrentCategory(final int category, final boolean force) {
+ if (mCurrentCategory == category && !force) {
+ return;
+ }
+
+ mCurrentCategory = category;
+ if (force || mEmojiPager.getCurrentItem() != category) {
+ mEmojiPager.setCurrentItem(category, true /* smoothScroll */);
+ }
+ if (force || mTabHost.getCurrentTab() != category) {
+ mTabHost.setCurrentTab(category);
+ }
+ // TODO: Record current category
+ }
+
+ private static class EmojiKeyboardAdapter extends PagerAdapter {
+ private final ScrollKeyboardView.OnKeyClickListener mListener;
+ private final KeyboardLayoutSet mLayoutSet;
+ private final RecentsKeyboard mRecentsKeyboard;
+ private final SparseArray<ScrollKeyboardView> mActiveKeyboardView =
+ CollectionUtils.newSparseArray();
+ private int mActivePosition = CATEGORY_UNSPECIFIED;
+
+ public EmojiKeyboardAdapter(final KeyboardLayoutSet layoutSet,
+ final ScrollKeyboardView.OnKeyClickListener listener) {
+ mListener = listener;
+ mLayoutSet = layoutSet;
+ mRecentsKeyboard = new RecentsKeyboard(
+ layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS));
+ }
+
+ public void addRecentKey(final Key key) {
+ if (mActivePosition == CATEGORY_RECENTS) {
+ return;
+ }
+ mRecentsKeyboard.addRecentKey(key);
+ final KeyboardView recentKeyboardView = mActiveKeyboardView.get(CATEGORY_RECENTS);
+ if (recentKeyboardView != null) {
+ recentKeyboardView.invalidateAllKeys();
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return sCategoryName.length;
+ }
+
+ @Override
+ public void setPrimaryItem(final View container, final int position, final Object object) {
+ if (mActivePosition == position) {
+ return;
+ }
+ final ScrollKeyboardView oldKeyboardView = mActiveKeyboardView.get(mActivePosition);
+ if (oldKeyboardView != null) {
+ oldKeyboardView.releaseCurrentKey();
+ oldKeyboardView.deallocateMemory();
+ }
+ mActivePosition = position;
+ }
+
+ @Override
+ public Object instantiateItem(final ViewGroup container, final int position) {
+ final int elementId = sCategoryElementId[position];
+ final Keyboard keyboard = (elementId == KeyboardId.ELEMENT_EMOJI_RECENTS)
+ ? mRecentsKeyboard : mLayoutSet.getKeyboard(elementId);
+ final LayoutInflater inflater = LayoutInflater.from(container.getContext());
+ final View view = inflater.inflate(
+ R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
+ final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById(
+ R.id.emoji_keyboard_page);
+ keyboardView.setKeyboard(keyboard);
+ keyboardView.setOnKeyClickListener(mListener);
+ final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById(
+ R.id.emoji_keyboard_scroller);
+ keyboardView.setScrollView(scrollView);
+ container.addView(view);
+ mActiveKeyboardView.put(position, keyboardView);
+ return view;
+ }
+
+ @Override
+ public boolean isViewFromObject(final View view, final Object object) {
+ return view == object;
+ }
+
+ @Override
+ public void destroyItem(final ViewGroup container, final int position,
+ final Object object) {
+ final ScrollKeyboardView keyboardView = mActiveKeyboardView.get(position);
+ if (keyboardView != null) {
+ keyboardView.deallocateMemory();
+ mActiveKeyboardView.remove(position);
+ }
+ container.removeView(keyboardView);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 61805286d..3ea68806b 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -58,12 +58,12 @@ public class Key implements Comparable<Key> {
/**
* The key code (unicode or custom code) that this key generates.
*/
- public final int mCode;
+ private final int mCode;
/** Label to display */
- public final String mLabel;
+ private final String mLabel;
/** Hint label to display on the key in conjunction with the label */
- public final String mHintLabel;
+ private final String mHintLabel;
/** Flags of the label */
private final int mLabelFlags;
private static final int LABEL_FLAGS_ALIGN_LEFT = 0x01;
@@ -95,18 +95,18 @@ public class Key implements Comparable<Key> {
private final int mIconId;
/** Width of the key, not including the gap */
- public final int mWidth;
+ private final int mWidth;
/** Height of the key, not including the gap */
- public final int mHeight;
+ private final int mHeight;
/** X coordinate of the key in the keyboard layout */
- public final int mX;
+ private final int mX;
/** Y coordinate of the key in the keyboard layout */
- public final int mY;
+ private final int mY;
/** Hit bounding box of the key */
- public final Rect mHitBox = new Rect();
+ private final Rect mHitBox = new Rect();
/** More keys. It is guaranteed that this is null or an array of one or more elements */
- public final MoreKeySpec[] mMoreKeys;
+ private final MoreKeySpec[] mMoreKeys;
/** More keys column number and flags */
private final int mMoreKeysColumnAndFlags;
private static final int MORE_KEYS_COLUMN_MASK = 0x000000ff;
@@ -121,12 +121,13 @@ public class Key implements Comparable<Key> {
private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
/** Background type that represents different key background visual than normal one. */
- public final int mBackgroundType;
- public static final int BACKGROUND_TYPE_NORMAL = 0;
- public static final int BACKGROUND_TYPE_FUNCTIONAL = 1;
- public static final int BACKGROUND_TYPE_ACTION = 2;
- public static final int BACKGROUND_TYPE_STICKY_OFF = 3;
- public static final int BACKGROUND_TYPE_STICKY_ON = 4;
+ private final int mBackgroundType;
+ public static final int BACKGROUND_TYPE_EMPTY = 0;
+ public static final int BACKGROUND_TYPE_NORMAL = 1;
+ public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
+ public static final int BACKGROUND_TYPE_ACTION = 3;
+ public static final int BACKGROUND_TYPE_STICKY_OFF = 4;
+ public static final int BACKGROUND_TYPE_STICKY_ON = 5;
private final int mActionFlags;
private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
@@ -134,7 +135,7 @@ public class Key implements Comparable<Key> {
private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
- public final KeyVisualAttributes mKeyVisualAttributes;
+ private final KeyVisualAttributes mKeyVisualAttributes;
private final OptionalAttributes mOptionalAttributes;
@@ -150,7 +151,7 @@ public class Key implements Comparable<Key> {
public final int mVisualInsetsLeft;
public final int mVisualInsetsRight;
- public OptionalAttributes(final String outputText, final int altCode,
+ private OptionalAttributes(final String outputText, final int altCode,
final int disabledIconId, final int previewIconId,
final int visualInsetsLeft, final int visualInsetsRight) {
mOutputText = outputText;
@@ -160,6 +161,18 @@ public class Key implements Comparable<Key> {
mVisualInsetsLeft = visualInsetsLeft;
mVisualInsetsRight = visualInsetsRight;
}
+
+ public static OptionalAttributes newInstance(final String outputText, final int altCode,
+ final int disabledIconId, final int previewIconId,
+ final int visualInsetsLeft, final int visualInsetsRight) {
+ if (outputText == null && altCode == CODE_UNSPECIFIED
+ && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
+ && visualInsetsLeft == 0 && visualInsetsRight == 0) {
+ return null;
+ }
+ return new OptionalAttributes(outputText, altCode, disabledIconId, previewIconId,
+ visualInsetsLeft, visualInsetsRight);
+ }
}
private final int mHashCode;
@@ -175,7 +188,7 @@ public class Key implements Comparable<Key> {
public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y,
final int width, final int height, final int labelFlags) {
this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode,
- moreKeySpec.mOutputText, x, y, width, height, labelFlags);
+ moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL);
}
/**
@@ -183,22 +196,19 @@ public class Key implements Comparable<Key> {
*/
public Key(final KeyboardParams params, final String label, final String hintLabel,
final int iconId, final int code, final String outputText, final int x, final int y,
- final int width, final int height, final int labelFlags) {
+ final int width, final int height, final int labelFlags, final int backgroundType) {
mHeight = height - params.mVerticalGap;
mWidth = width - params.mHorizontalGap;
mHintLabel = hintLabel;
mLabelFlags = labelFlags;
- mBackgroundType = BACKGROUND_TYPE_NORMAL;
+ mBackgroundType = backgroundType;
mActionFlags = 0;
mMoreKeys = null;
mMoreKeysColumnAndFlags = 0;
mLabel = label;
- if (outputText == null) {
- mOptionalAttributes = null;
- } else {
- mOptionalAttributes = new OptionalAttributes(outputText, CODE_UNSPECIFIED,
- ICON_UNDEFINED, ICON_UNDEFINED, 0, 0);
- }
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED,
+ ICON_UNDEFINED, ICON_UNDEFINED,
+ 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */);
mCode = code;
mEnabled = (code != CODE_UNSPECIFIED);
mIconId = iconId;
@@ -224,7 +234,7 @@ public class Key implements Comparable<Key> {
public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
final XmlPullParser parser) throws XmlPullParserException {
final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
- final int rowHeight = row.mRowHeight;
+ final int rowHeight = row.getRowHeight();
mHeight = rowHeight - params.mVerticalGap;
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
@@ -259,11 +269,11 @@ public class Key implements Comparable<Key> {
final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIconPreview));
- mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
+ mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
| row.getDefaultKeyLabelFlags();
final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId);
final Locale locale = params.mId.mLocale;
- int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
int moreKeysColumn = style.getInt(keyAttr,
@@ -359,15 +369,8 @@ public class Key implements Comparable<Key> {
KeySpecParser.parseCode(style.getString(keyAttr,
R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
needsToUpperCase, locale);
- if (outputText == null && altCode == CODE_UNSPECIFIED
- && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
- && visualInsetsLeft == 0 && visualInsetsRight == 0) {
- mOptionalAttributes = null;
- } else {
- mOptionalAttributes = new OptionalAttributes(outputText, altCode,
- disabledIconId, previewIconId,
- visualInsetsLeft, visualInsetsRight);
- }
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
+ disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
keyAttr.recycle();
mHashCode = computeHashCode(this);
@@ -376,6 +379,35 @@ public class Key implements Comparable<Key> {
}
}
+ /**
+ * Copy constructor.
+ *
+ * @param key the original key.
+ */
+ protected Key(final Key key) {
+ // Final attributes.
+ mCode = key.mCode;
+ mLabel = key.mLabel;
+ mHintLabel = key.mHintLabel;
+ mLabelFlags = key.mLabelFlags;
+ mIconId = key.mIconId;
+ mWidth = key.mWidth;
+ mHeight = key.mHeight;
+ mX = key.mX;
+ mY = key.mY;
+ mHitBox.set(key.mHitBox);
+ mMoreKeys = key.mMoreKeys;
+ mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
+ mBackgroundType = key.mBackgroundType;
+ mActionFlags = key.mActionFlags;
+ mKeyVisualAttributes = key.mKeyVisualAttributes;
+ mOptionalAttributes = key.mOptionalAttributes;
+ mHashCode = key.mHashCode;
+ // Key state.
+ mPressed = key.mPressed;
+ mEnabled = key.mEnabled;
+ }
+
private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
switch (keyboardElementId) {
@@ -465,6 +497,7 @@ public class Key implements Comparable<Key> {
private static String backgroundName(final int backgroundType) {
switch (backgroundType) {
+ case BACKGROUND_TYPE_EMPTY: return "empty";
case BACKGROUND_TYPE_NORMAL: return "normal";
case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
case BACKGROUND_TYPE_ACTION: return "action";
@@ -474,6 +507,22 @@ public class Key implements Comparable<Key> {
}
}
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getHintLabel() {
+ return mHintLabel;
+ }
+
+ public MoreKeySpec[] getMoreKeys() {
+ return mMoreKeys;
+ }
+
public void markAsLeftEdge(final KeyboardParams params) {
mHitBox.left = params.mLeftPadding;
}
@@ -520,6 +569,10 @@ public class Key implements Comparable<Key> {
&& (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
}
+ public KeyVisualAttributes getVisualAttributes() {
+ return mKeyVisualAttributes;
+ }
+
public final Typeface selectTypeface(final KeyDrawParams params) {
// TODO: Handle "bold" here too?
if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
@@ -694,9 +747,26 @@ public class Key implements Comparable<Key> {
? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
}
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getX() {
+ return mX;
+ }
+
+ public int getY() {
+ return mY;
+ }
+
public final int getDrawX() {
+ final int x = getX();
final OptionalAttributes attrs = mOptionalAttributes;
- return (attrs == null) ? mX : mX + attrs.mVisualInsetsLeft;
+ return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
}
public final int getDrawWidth() {
@@ -731,6 +801,10 @@ public class Key implements Comparable<Key> {
mEnabled = enabled;
}
+ public Rect getHitBox() {
+ return mHitBox;
+ }
+
/**
* Detects if a point falls on this key.
* @param x the x-coordinate of the point
@@ -750,9 +824,9 @@ public class Key implements Comparable<Key> {
* @return the square of the distance of the point from the nearest edge of the key
*/
public int squaredDistanceToEdge(final int x, final int y) {
- final int left = mX;
+ final int left = getX();
final int right = left + mWidth;
- final int top = mY;
+ final int top = getY();
final int bottom = top + mHeight;
final int edgeX = x < left ? left : (x > right ? right : x);
final int edgeY = y < top ? top : (y > bottom ? bottom : y);
@@ -788,6 +862,10 @@ public class Key implements Comparable<Key> {
android.R.attr.state_pressed
};
+ private final static int[] KEY_STATE_EMPTY = {
+ android.R.attr.state_empty
+ };
+
// functional normal state (with properties)
private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
android.R.attr.state_single
@@ -825,6 +903,8 @@ public class Key implements Comparable<Key> {
return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
case BACKGROUND_TYPE_STICKY_ON:
return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
+ case BACKGROUND_TYPE_EMPTY:
+ return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY;
default: /* BACKGROUND_TYPE_NORMAL */
return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
}
@@ -842,7 +922,7 @@ public class Key implements Comparable<Key> {
protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
final int height) {
super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
- null, x, y, width, height, 0);
+ null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY);
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 17e707f95..befb6fa92 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -108,8 +108,9 @@ public class KeyDetector {
if (distance > minDistance) {
continue;
}
- // To take care of hitbox overlaps, we compare mCode here too.
- if (primaryKey == null || distance < minDistance || key.mCode > primaryKey.mCode) {
+ // To take care of hitbox overlaps, we compare key's code here too.
+ if (primaryKey == null || distance < minDistance
+ || key.getCode() > primaryKey.getCode()) {
minDistance = distance;
primaryKey = key;
}
@@ -118,7 +119,7 @@ public class KeyDetector {
}
public static String printableCode(Key key) {
- return key != null ? Constants.printableCode(key.mCode) : "none";
+ return key != null ? Constants.printableCode(key.getCode()) : "none";
}
public static String printableCodes(int[] codes) {
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index fefac96fd..23f037fbd 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -51,6 +51,11 @@ public class Keyboard {
/** Total width of the keyboard, including the padding and keys */
public final int mOccupiedWidth;
+ /** Base height of the keyboard, used to calculate rows' height */
+ public final int mBaseHeight;
+ /** Base width of the keyboard, used to calculate keys' width */
+ public final int mBaseWidth;
+
/** The padding above the keyboard */
public final int mTopPadding;
/** Default gap between rows */
@@ -69,7 +74,7 @@ public class Keyboard {
public final int mMaxMoreKeysKeyboardColumn;
/** Array of keys and icons in this keyboard */
- public final Key[] mKeys;
+ private final Key[] mKeys;
public final Key[] mShiftKeys;
public final Key[] mAltCodeKeysWhileTyping;
public final KeyboardIconsSet mIconsSet;
@@ -84,6 +89,8 @@ public class Keyboard {
mThemeId = params.mThemeId;
mOccupiedHeight = params.mOccupiedHeight;
mOccupiedWidth = params.mOccupiedWidth;
+ mBaseHeight = params.mBaseHeight;
+ mBaseWidth = params.mBaseWidth;
mMostCommonKeyHeight = params.mMostCommonKeyHeight;
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
mMoreKeysTemplate = params.mMoreKeysTemplate;
@@ -104,6 +111,30 @@ public class Keyboard {
mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
}
+ protected Keyboard(final Keyboard keyboard) {
+ mId = keyboard.mId;
+ mThemeId = keyboard.mThemeId;
+ mOccupiedHeight = keyboard.mOccupiedHeight;
+ mOccupiedWidth = keyboard.mOccupiedWidth;
+ mBaseHeight = keyboard.mBaseHeight;
+ mBaseWidth = keyboard.mBaseWidth;
+ mMostCommonKeyHeight = keyboard.mMostCommonKeyHeight;
+ mMostCommonKeyWidth = keyboard.mMostCommonKeyWidth;
+ mMoreKeysTemplate = keyboard.mMoreKeysTemplate;
+ mMaxMoreKeysKeyboardColumn = keyboard.mMaxMoreKeysKeyboardColumn;
+ mKeyVisualAttributes = keyboard.mKeyVisualAttributes;
+ mTopPadding = keyboard.mTopPadding;
+ mVerticalGap = keyboard.mVerticalGap;
+
+ mKeys = keyboard.mKeys;
+ mShiftKeys = keyboard.mShiftKeys;
+ mAltCodeKeysWhileTyping = keyboard.mAltCodeKeysWhileTyping;
+ mIconsSet = keyboard.mIconsSet;
+
+ mProximityInfo = keyboard.mProximityInfo;
+ mProximityCharsCorrectionEnabled = keyboard.mProximityCharsCorrectionEnabled;
+ }
+
public boolean hasProximityCharsCorrection(final int code) {
if (!mProximityCharsCorrectionEnabled) {
return false;
@@ -120,6 +151,10 @@ public class Keyboard {
return mProximityInfo;
}
+ public Key[] getKeys() {
+ return mKeys;
+ }
+
public Key getKey(final int code) {
if (code == Constants.CODE_UNSPECIFIED) {
return null;
@@ -130,8 +165,8 @@ public class Keyboard {
return mKeyCache.valueAt(index);
}
- for (final Key key : mKeys) {
- if (key.mCode == code) {
+ for (final Key key : getKeys()) {
+ if (key.getCode() == code) {
mKeyCache.put(code, key);
return key;
}
@@ -146,9 +181,9 @@ public class Keyboard {
return true;
}
- for (final Key key : mKeys) {
+ for (final Key key : getKeys()) {
if (key == aKey) {
- mKeyCache.put(key.mCode, key);
+ mKeyCache.put(key.getCode(), key);
return true;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index b26698665..dc760e685 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -26,10 +26,10 @@ public interface KeyboardActionListener {
*
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
* the value will be zero.
- * @param isRepeatKey true if pressing has occurred while key repeat input.
+ * @param repeatCount how many times the key was repeated. Zero if it is the first press.
* @param isSinglePointer true if pressing has occurred while no other key is being pressed.
*/
- public void onPressKey(int primaryCode, boolean isRepeatKey, boolean isSinglePointer);
+ public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer);
/**
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -103,7 +103,7 @@ public interface KeyboardActionListener {
public static class Adapter implements KeyboardActionListener {
@Override
- public void onPressKey(int primaryCode, boolean isRepeatKey, boolean isSinglePointer) {}
+ public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {}
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 1dc3c6a4c..53748bb58 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -50,10 +50,16 @@ public final class KeyboardId {
public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3;
public static final int ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED = 4;
public static final int ELEMENT_SYMBOLS = 5;
- public static final int ELEMENT_SYMBOLS_SHIFTED = 6;
public static final int ELEMENT_PHONE = 7;
public static final int ELEMENT_PHONE_SYMBOLS = 8;
public static final int ELEMENT_NUMBER = 9;
+ public static final int ELEMENT_EMOJI_RECENTS = 10;
+ public static final int ELEMENT_EMOJI_CATEGORY1 = 11;
+ public static final int ELEMENT_EMOJI_CATEGORY2 = 12;
+ public static final int ELEMENT_EMOJI_CATEGORY3 = 13;
+ public static final int ELEMENT_EMOJI_CATEGORY4 = 14;
+ public static final int ELEMENT_EMOJI_CATEGORY5 = 15;
+ public static final int ELEMENT_EMOJI_CATEGORY6 = 16;
public final InputMethodSubtype mSubtype;
public final Locale mLocale;
@@ -213,10 +219,16 @@ public final class KeyboardId {
case ELEMENT_ALPHABET_SHIFT_LOCKED: return "alphabetShiftLocked";
case ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: return "alphabetShiftLockShifted";
case ELEMENT_SYMBOLS: return "symbols";
- case ELEMENT_SYMBOLS_SHIFTED: return "symbolsShifted";
case ELEMENT_PHONE: return "phone";
case ELEMENT_PHONE_SYMBOLS: return "phoneSymbols";
case ELEMENT_NUMBER: return "number";
+ case ELEMENT_EMOJI_RECENTS: return "emojiRecents";
+ case ELEMENT_EMOJI_CATEGORY1: return "emojiCategory1";
+ case ELEMENT_EMOJI_CATEGORY2: return "emojiCategory2";
+ case ELEMENT_EMOJI_CATEGORY3: return "emojiCategory3";
+ case ELEMENT_EMOJI_CATEGORY4: return "emojiCategory4";
+ case ELEMENT_EMOJI_CATEGORY5: return "emojiCategory5";
+ case ELEMENT_EMOJI_CATEGORY6: return "emojiCategory6";
default: return null;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index e97f29452..711de63b3 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -162,7 +162,8 @@ public final class KeyboardLayoutSet {
final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
try {
return getKeyboard(elementParams, id);
- } catch (RuntimeException e) {
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Can't create keyboard: " + id, e);
throw new KeyboardLayoutSetException(e, id);
}
}
@@ -213,7 +214,6 @@ public final class KeyboardLayoutSet {
private final Context mContext;
private final String mPackageName;
private final Resources mResources;
- private final EditorInfo mEditorInfo;
private final Params mParams = new Params();
@@ -223,13 +223,12 @@ public final class KeyboardLayoutSet {
mContext = context;
mPackageName = context.getPackageName();
mResources = context.getResources();
- mEditorInfo = editorInfo;
final Params params = mParams;
params.mMode = getKeyboardMode(editorInfo);
params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
- mPackageName, NO_SETTINGS_KEY, mEditorInfo);
+ mPackageName, NO_SETTINGS_KEY, params.mEditorInfo);
}
public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
@@ -242,7 +241,7 @@ public final class KeyboardLayoutSet {
final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
@SuppressWarnings("deprecation")
final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
- mPackageName, FORCE_ASCII, mEditorInfo);
+ mPackageName, FORCE_ASCII, mParams.mEditorInfo);
final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
mParams.mEditorInfo.imeOptions)
|| deprecatedForceAscii;
@@ -264,9 +263,9 @@ public final class KeyboardLayoutSet {
final boolean languageSwitchKeyEnabled) {
@SuppressWarnings("deprecation")
final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
- null, NO_MICROPHONE_COMPAT, mEditorInfo);
+ null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo);
final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
- mPackageName, NO_MICROPHONE, mEditorInfo)
+ mPackageName, NO_MICROPHONE, mParams.mEditorInfo)
|| deprecatedNoMicrophone;
mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
mParams.mVoiceKeyOnMain = voiceKeyOnMain;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 1ea0f8b96..c319c57a1 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -58,19 +58,17 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
private static final KeyboardTheme[] KEYBOARD_THEMES = {
- new KeyboardTheme(0, R.style.KeyboardTheme),
- new KeyboardTheme(1, R.style.KeyboardTheme_HighContrast),
- new KeyboardTheme(6, R.style.KeyboardTheme_Stone),
- new KeyboardTheme(7, R.style.KeyboardTheme_Stone_Bold),
- new KeyboardTheme(8, R.style.KeyboardTheme_Gingerbread),
- new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
+ new KeyboardTheme(0, R.style.KeyboardTheme_ICS),
+ new KeyboardTheme(1, R.style.KeyboardTheme_GB),
};
private SubtypeSwitcher mSubtypeSwitcher;
private SharedPreferences mPrefs;
private InputView mCurrentInputView;
+ private View mMainKeyboardFrame;
private MainKeyboardView mKeyboardView;
+ private EmojiKeyboardView mEmojiKeyboardView;
private LatinIME mLatinIME;
private Resources mResources;
@@ -121,8 +119,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
} catch (NumberFormatException e) {
// Format error, keyboard theme is default to 0.
}
- Log.w(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to 0");
- return KEYBOARD_THEMES[0];
+ Log.w(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to "
+ + defaultIndex);
+ return KEYBOARD_THEMES[Integer.valueOf(defaultIndex)];
}
private void setContextThemeWrapper(final Context context, final KeyboardTheme keyboardTheme) {
@@ -170,6 +169,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
private void setKeyboard(final Keyboard keyboard) {
+ // Make {@link MainKeyboardView} visible and hide {@link EmojiKeyboardView}.
+ setMainKeyboardFrame();
final MainKeyboardView keyboardView = mKeyboardView;
final Keyboard oldKeyboard = keyboardView.getKeyboard();
keyboardView.setKeyboard(keyboard);
@@ -256,10 +257,16 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS));
}
+ private void setMainKeyboardFrame() {
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ mEmojiKeyboardView.setVisibility(View.GONE);
+ }
+
// Implements {@link KeyboardState.SwitchActions}.
@Override
- public void setSymbolsShiftedKeyboard() {
- setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED));
+ public void setEmojiKeyboard() {
+ mMainKeyboardFrame.setVisibility(View.GONE);
+ mEmojiKeyboardView.setVisibility(View.VISIBLE);
}
// Implements {@link KeyboardState.SwitchActions}.
@@ -301,6 +308,20 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
}
+ public boolean isShowingMoreKeysPanel() {
+ if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) {
+ return false;
+ }
+ return mKeyboardView.isShowingMoreKeysPanel();
+ }
+
+ public View getVisibleKeyboardView() {
+ if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) {
+ return mEmojiKeyboardView;
+ }
+ return mKeyboardView;
+ }
+
public MainKeyboardView getMainKeyboardView() {
return mKeyboardView;
}
@@ -313,10 +334,16 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
setContextThemeWrapper(mLatinIME, mKeyboardTheme);
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
R.layout.input_view, null);
+ mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
+ mEmojiKeyboardView = (EmojiKeyboardView)mCurrentInputView.findViewById(
+ R.id.emoji_keyboard_view);
mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
mKeyboardView.setKeyboardActionListener(mLatinIME);
+ mEmojiKeyboardView.setHardwareAcceleratedDrawingEnabled(
+ isHardwareAcceleratedDrawingEnabled);
+ mEmojiKeyboardView.setKeyboardActionListener(mLatinIME);
// This always needs to be set since the accessibility state can
// potentially change without the input view being re-created.
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 054c503d8..0ef6802ca 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -196,13 +196,14 @@ public class KeyboardView extends View {
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- if (mKeyboard != null) {
- // The main keyboard expands to the display width.
- final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
- setMeasuredDimension(widthMeasureSpec, height);
- } else {
+ if (mKeyboard == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
}
+ // The main keyboard expands to the entire this {@link KeyboardView}.
+ final int width = mKeyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
+ final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
}
@Override
@@ -264,9 +265,9 @@ public class KeyboardView extends View {
mClipRegion.setEmpty();
for (final Key key : mInvalidatedKeys) {
if (mKeyboard.hasKey(key)) {
- final int x = key.mX + getPaddingLeft();
- final int y = key.mY + getPaddingTop();
- mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
+ final int x = key.getX() + getPaddingLeft();
+ final int y = key.getY() + getPaddingTop();
+ mWorkingRect.set(x, y, x + key.getWidth(), y + key.getHeight());
mClipRegion.union(mWorkingRect);
}
}
@@ -284,7 +285,7 @@ public class KeyboardView extends View {
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
if (drawAllKeys || isHardwareAccelerated) {
// Draw all keys.
- for (final Key key : mKeyboard.mKeys) {
+ for (final Key key : mKeyboard.getKeys()) {
onDrawKey(key, canvas, paint);
}
} else {
@@ -309,11 +310,11 @@ public class KeyboardView extends View {
private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
final int keyDrawX = key.getDrawX() + getPaddingLeft();
- final int keyDrawY = key.mY + getPaddingTop();
+ final int keyDrawY = key.getY() + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY);
final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
- final KeyVisualAttributes attr = key.mKeyVisualAttributes;
+ final KeyVisualAttributes attr = key.getVisualAttributes();
final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
@@ -329,7 +330,7 @@ public class KeyboardView extends View {
protected void onDrawKeyBackground(final Key key, final Canvas canvas) {
final Rect padding = mKeyBackgroundPadding;
final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
- final int bgHeight = key.mHeight + padding.top + padding.bottom;
+ final int bgHeight = key.getHeight() + padding.top + padding.bottom;
final int bgX = -padding.left;
final int bgY = -padding.top;
final int[] drawableState = key.getCurrentDrawableState();
@@ -351,7 +352,7 @@ public class KeyboardView extends View {
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
- final int keyHeight = key.mHeight;
+ final int keyHeight = key.getHeight();
final float centerX = keyWidth * 0.5f;
final float centerY = keyHeight * 0.5f;
@@ -362,8 +363,8 @@ public class KeyboardView extends View {
// Draw key label.
final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha);
float positionX = centerX;
- if (key.mLabel != null) {
- final String label = key.mLabel;
+ final String label = key.getLabel();
+ if (label != null) {
paint.setTypeface(key.selectTypeface(params));
paint.setTextSize(key.selectTextSize(params));
final float labelCharHeight = TypefaceUtils.getCharHeight(
@@ -440,8 +441,8 @@ public class KeyboardView extends View {
}
// Draw hint label.
- if (key.mHintLabel != null) {
- final String hintLabel = key.mHintLabel;
+ final String hintLabel = key.getHintLabel();
+ if (hintLabel != null) {
paint.setTextSize(key.selectHintTextSize(params));
paint.setColor(key.selectHintTextColor(params));
blendAlpha(paint, params.mAnimAlpha);
@@ -480,7 +481,7 @@ public class KeyboardView extends View {
}
// Draw key icon.
- if (key.mLabel == null && icon != null) {
+ if (label == null && icon != null) {
final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
final int iconHeight = icon.getIntrinsicHeight();
final int iconX, alignX;
@@ -504,7 +505,7 @@ public class KeyboardView extends View {
}
}
- if (key.hasPopupHint() && key.mMoreKeys != null) {
+ if (key.hasPopupHint() && key.getMoreKeys() != null) {
drawKeyPopupHint(key, canvas, paint, params);
}
}
@@ -513,7 +514,7 @@ public class KeyboardView extends View {
protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
- final int keyHeight = key.mHeight;
+ final int keyHeight = key.getHeight();
paint.setTypeface(params.mTypeface);
paint.setTextSize(params.mHintLetterSize);
@@ -601,9 +602,9 @@ public class KeyboardView extends View {
if (mInvalidateAllKeys) return;
if (key == null) return;
mInvalidatedKeys.add(key);
- final int x = key.mX + getPaddingLeft();
- final int y = key.mY + getPaddingTop();
- invalidate(x, y, x + key.mWidth, y + key.mHeight);
+ final int x = key.getX() + getPaddingLeft();
+ final int y = key.getY() + getPaddingTop();
+ invalidate(x, y, x + key.getWidth(), y + key.getHeight());
}
@Override
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 526c2f1ec..e4a8f4cb2 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -217,7 +217,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
startWhileTypingFadeinAnimation(keyboardView);
break;
case MSG_REPEAT_KEY:
- tracker.onKeyRepeat(msg.arg1);
+ tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
break;
case MSG_LONGPRESS_KEY:
keyboardView.onLongPress(tracker);
@@ -230,12 +230,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void startKeyRepeatTimer(final PointerTracker tracker, final int delay) {
+ public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount,
+ final int delay) {
final Key key = tracker.getKey();
if (key == null || delay == 0) {
return;
}
- sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
+ sendMessageDelayed(
+ obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay);
}
public void cancelKeyRepeatTimer() {
@@ -296,7 +298,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final MainKeyboardView keyboardView = getOuterInstance();
// When user hits the space or the enter key, just cancel the while-typing timer.
- final int typedCode = typedKey.mCode;
+ final int typedCode = typedKey.getCode();
if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
if (isTyping) {
startWhileTypingFadeinAnimation(keyboardView);
@@ -636,7 +638,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mKeyPreviewLingerTimeout = delay;
}
-
private void locatePreviewPlacerView() {
if (mPreviewPlacerView.getParent() != null) {
return;
@@ -805,11 +806,11 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.
- final int previewY = key.mY - previewHeight + mKeyPreviewOffset
+ final int previewY = key.getY() - previewHeight + mKeyPreviewOffset
+ CoordinateUtils.y(mOriginCoords);
if (background != null) {
- final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
+ final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
background.setAlpha(PREVIEW_ALPHA);
}
@@ -901,7 +902,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) {
- if (key.mMoreKeys == null) {
+ if (key.getMoreKeys() == null) {
return null;
}
Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
@@ -936,15 +937,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
final KeyboardActionListener listener = mKeyboardActionListener;
if (key.hasNoPanelAutoMoreKey()) {
- final int moreKeyCode = key.mMoreKeys[0].mCode;
+ final int moreKeyCode = key.getMoreKeys()[0].mCode;
tracker.onLongPressed();
- listener.onPressKey(moreKeyCode, false /* isRepeatKey */, true /* isSinglePointer */);
+ listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */);
listener.onCodeInput(moreKeyCode,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
listener.onReleaseKey(moreKeyCode, false /* withSliding */);
return;
}
- final int code = key.mCode;
+ final int code = key.getCode();
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
// Long pressing the space key invokes IME switcher dialog.
if (listener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
@@ -970,13 +971,13 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
// keys keyboard is placed at the touch point of the parent key.
final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
? CoordinateUtils.x(lastCoords)
- : key.mX + key.mWidth / 2;
+ : key.getX() + key.getWidth() / 2;
// The more keys keyboard is usually vertically aligned with the top edge of the parent key
// (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
// aligned with the bottom edge of the visible part of the key preview.
// {@code mPreviewVisibleOffset} has been set appropriately in
// {@link KeyboardView#showKeyPreview(PointerTracker)}.
- final int pointY = key.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
+ final int pointY = key.getY() + mKeyPreviewDrawParams.mPreviewVisibleOffset;
moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
tracker.onShowMoreKeysPanel(moreKeysPanel);
}
@@ -1172,13 +1173,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (key.altCodeWhileTyping() && key.isEnabled()) {
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
}
- if (key.mCode == Constants.CODE_SPACE) {
+ final int code = key.getCode();
+ if (code == Constants.CODE_SPACE) {
drawSpacebar(key, canvas, paint);
// Whether space key needs to show the "..." popup hint for special purposes
if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
drawKeyPopupHint(key, canvas, paint, params);
}
- } else if (key.mCode == Constants.CODE_LANGUAGE_SWITCH) {
+ } else if (code == Constants.CODE_LANGUAGE_SWITCH) {
super.onDrawKeyTopVisuals(key, canvas, paint, params);
drawKeyPopupHint(key, canvas, paint, params);
} else {
@@ -1226,8 +1228,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
- final int width = key.mWidth;
- final int height = key.mHeight;
+ final int width = key.getWidth();
+ final int height = key.getHeight();
// If input language are explicitly selected.
if (mNeedsToDisplayLanguage) {
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
index a2001cb8f..6b76e2461 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
@@ -39,7 +39,7 @@ public final class MoreKeysDetector extends KeyDetector {
Key nearestKey = null;
int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
- for (final Key key : getKeyboard().mKeys) {
+ for (final Key key : getKeyboard().getKeys()) {
final int dist = key.squaredDistanceToEdge(touchX, touchY);
if (dist < nearestDist) {
nearestKey = key;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index 3fd29dcfd..8256d4623 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -276,6 +276,7 @@ public final class MoreKeysKeyboard extends Keyboard {
mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2;
mParentKey = parentKey;
+ final MoreKeySpec[] moreKeys = parentKey.getMoreKeys();
final int width, height;
// {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at
// {@link MainKeyboardView#showKeyPreview(PointerTracker}, though there may be
@@ -283,7 +284,7 @@ public final class MoreKeysKeyboard extends Keyboard {
// zero-division error at
// {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}.
final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled()
- && !parentKey.noKeyPreview() && parentKey.mMoreKeys.length == 1
+ && !parentKey.noKeyPreview() && moreKeys.length == 1
&& keyPreviewDrawParams.mPreviewVisibleWidth > 0;
if (singleMoreKeyWithPreview) {
// Use pre-computed width and height if this more keys keyboard has only one key to
@@ -312,8 +313,8 @@ public final class MoreKeysKeyboard extends Keyboard {
mDivider = null;
dividerWidth = 0;
}
- mParams.setParameters(parentKey.mMoreKeys.length, parentKey.getMoreKeysColumn(),
- width, height, parentKey.mX + parentKey.mWidth / 2,
+ mParams.setParameters(moreKeys.length, parentKey.getMoreKeysColumn(),
+ width, height, parentKey.getX() + parentKey.getWidth() / 2,
parentKeyboard.mId.mWidth, parentKey.isFixedColumnOrderMoreKeys(),
dividerWidth);
}
@@ -321,7 +322,7 @@ public final class MoreKeysKeyboard extends Keyboard {
private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
final float padding, final Paint paint) {
int maxWidth = minKeyWidth;
- for (final MoreKeySpec spec : parentKey.mMoreKeys) {
+ for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
final String label = spec.mLabel;
// If the label is single letter, minKeyWidth is enough to hold the label.
if (label != null && StringUtils.codePointCount(label) > 1) {
@@ -336,7 +337,7 @@ public final class MoreKeysKeyboard extends Keyboard {
public MoreKeysKeyboard build() {
final MoreKeysKeyboardParams params = mParams;
final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
- final MoreKeySpec[] moreKeys = mParentKey.mMoreKeys;
+ final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
for (int n = 0; n < moreKeys.length; n++) {
final MoreKeySpec moreKeySpec = moreKeys[n];
final int row = n / params.mNumColumns;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index f00f5a99e..973128d36 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -127,7 +127,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) {
if (mCurrentKey != null && mActivePointerId == pointerId) {
updateReleaseKeyGraphics(mCurrentKey);
- onCodeInput(mCurrentKey.mCode, x, y);
+ onCodeInput(mCurrentKey.getCode(), x, y);
mCurrentKey = null;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index b66ee2a65..d4d0d8718 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -64,7 +64,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
/**
* Get KeyboardActionListener object that is used to register key code and so on.
- * @return the KeyboardActionListner for this PointerTracker
+ * @return the KeyboardActionListner for this PointerTracke
*/
public KeyboardActionListener getKeyboardActionListener();
@@ -94,7 +94,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
public interface TimerProxy {
public void startTypingStateTimer(Key typedKey);
public boolean isTypingState();
- public void startKeyRepeatTimer(PointerTracker tracker, int delay);
+ public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay);
public void startLongPressTimer(PointerTracker tracker, int delay);
public void cancelLongPressTimer();
public void startDoubleTapShiftKeyTimer();
@@ -111,7 +111,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
@Override
public boolean isTypingState() { return false; }
@Override
- public void startKeyRepeatTimer(PointerTracker tracker, int delay) {}
+ public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay) {}
@Override
public void startLongPressTimer(PointerTracker tracker, int delay) {}
@Override
@@ -490,7 +490,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// Returns true if keyboard has been changed by this callback.
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
- final boolean isRepeatKey) {
+ final int repeatCount) {
// While gesture input is going on, this method should be a no-operation. But when gesture
// input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
// are set to false. To keep this method is a no-operation,
@@ -504,13 +504,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
KeyDetector.printableCode(key),
ignoreModifierKey ? " ignoreModifier" : "",
key.isEnabled() ? "" : " disabled",
- isRepeatKey ? " repeat" : ""));
+ repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
}
if (ignoreModifierKey) {
return false;
}
if (key.isEnabled()) {
- mListener.onPressKey(key.mCode, isRepeatKey, getActivePointerTrackerCount() == 1);
+ mListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
mTimerProxy.startTypingStateTimer(key);
@@ -776,7 +776,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
return;
}
- if (key == null || !Character.isLetter(key.mCode)) {
+ if (key == null || !Character.isLetter(key.getCode())) {
return;
}
if (DEBUG_LISTENER) {
@@ -967,7 +967,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// This onPress call may have changed keyboard layout. Those cases are detected at
// {@link #setKeyboard}. In those cases, we should update key according to the new
// keyboard layout.
- if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onDownKey(x, y, eventTime);
}
@@ -1057,7 +1057,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// at {@link #setKeyboard}. In those cases, we should update key according
// to the new keyboard layout.
Key key = newKey;
- if (callListenerOnPressAndCheckKeyboardLayoutChange(key, false /* isRepeatKey */)) {
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onMoveKey(x, y);
}
onMoveToNewKey(key, x, y);
@@ -1075,8 +1075,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
+ " phantom sudden move event (distance=%d) is translated to "
+ "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
getDistance(x, y, lastX, lastY),
- lastX, lastY, Constants.printableCode(oldKey.mCode),
- x, y, Constants.printableCode(key.mCode)));
+ lastX, lastY, Constants.printableCode(oldKey.getCode()),
+ x, y, Constants.printableCode(key.getCode())));
}
// TODO: This should be moved to outside of this nested if-clause?
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -1098,8 +1098,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
+ " bogus down-move-up event (raidus=%.2f key diagonal) is "
+ " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
mPointerId, radiusRatio,
- lastX, lastY, Constants.printableCode(oldKey.mCode),
- x, y, Constants.printableCode(key.mCode)));
+ lastX, lastY, Constants.printableCode(oldKey.getCode()),
+ x, y, Constants.printableCode(key.getCode())));
}
onUpEventInternal(x, y, eventTime);
onDownEventInternal(x, y, eventTime);
@@ -1107,7 +1107,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
private void processSildeOutFromOldKey(final Key oldKey) {
setReleasedKeyGraphics(oldKey);
- callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */);
+ callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
startSlidingKeyInput(oldKey);
mTimerProxy.cancelKeyTimers();
}
@@ -1263,7 +1263,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (sInGesture) {
if (currentKey != null) {
- callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */);
+ callListenerOnRelease(currentKey, currentKey.getCode(), true /* withSliding */);
}
mayEndBatchInput(eventTime);
return;
@@ -1376,9 +1376,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// doesn't have its more keys. (e.g. spacebar, globe key)
// We always need to start the long press timer if the key has its more keys regardless of
// whether or not we are in the sliding input mode.
- if (mIsInSlidingKeyInput && key.mMoreKeys == null) return;
+ if (mIsInSlidingKeyInput && key.getMoreKeys() == null) return;
final int delay;
- switch (key.mCode) {
+ switch (key.getCode()) {
case Constants.CODE_SHIFT:
delay = sParams.mLongPressShiftLockTimeout;
break;
@@ -1401,7 +1401,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return;
}
- final int code = key.mCode;
+ final int code = key.getCode();
callListenerOnCodeInput(key, code, x, y, eventTime);
callListenerOnRelease(key, code, false /* withSliding */);
}
@@ -1412,17 +1412,19 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (!key.isRepeatable()) return;
// Don't start key repeat when we are in sliding input mode.
if (mIsInSlidingKeyInput) return;
- detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
- mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatStartTimeout);
+ detectAndSendKey(key, key.getX(), key.getY(), SystemClock.uptimeMillis());
+ final int startRepeatCount = 1;
+ mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout);
}
- public void onKeyRepeat(final int code) {
+ public void onKeyRepeat(final int code, final int repeatCount) {
final Key key = getKey();
- if (key == null || key.mCode != code) {
+ if (key == null || key.getCode() != code) {
return;
}
- mTimerProxy.startKeyRepeatTimer(this, sParams.mKeyRepeatInterval);
- callListenerOnPressAndCheckKeyboardLayoutChange(key, true /* isRepeatKey */);
+ final int nextRepeatCount = repeatCount + 1;
+ mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval);
+ callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis());
}
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 9b0a33cec..cd127c760 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -96,7 +96,7 @@ public class ProximityInfo {
private static boolean needsProximityInfo(final Key key) {
// Don't include special keys into ProximityInfo.
- return key.mCode >= Constants.CODE_SPACE;
+ return key.getCode() >= Constants.CODE_SPACE;
}
private static int getProximityInfoKeysCount(final Key[] keys) {
@@ -122,7 +122,7 @@ public class ProximityInfo {
if (!needsProximityInfo(neighborKey)) {
continue;
}
- proximityCharsArray[infoIndex] = neighborKey.mCode;
+ proximityCharsArray[infoIndex] = neighborKey.getCode();
infoIndex++;
}
}
@@ -159,11 +159,11 @@ public class ProximityInfo {
if (!needsProximityInfo(key)) {
continue;
}
- keyXCoordinates[infoIndex] = key.mX;
- keyYCoordinates[infoIndex] = key.mY;
- keyWidths[infoIndex] = key.mWidth;
- keyHeights[infoIndex] = key.mHeight;
- keyCharCodes[infoIndex] = key.mCode;
+ keyXCoordinates[infoIndex] = key.getX();
+ keyYCoordinates[infoIndex] = key.getY();
+ keyWidths[infoIndex] = key.getWidth();
+ keyHeights[infoIndex] = key.getHeight();
+ keyCharCodes[infoIndex] = key.getCode();
infoIndex++;
}
@@ -183,7 +183,7 @@ public class ProximityInfo {
if (!needsProximityInfo(key)) {
continue;
}
- final Rect hitBox = key.mHitBox;
+ final Rect hitBox = key.getHitBox();
sweetSpotCenterXs[infoIndex] = hitBox.exactCenterX();
sweetSpotCenterYs[infoIndex] = hitBox.exactCenterY();
sweetSpotRadii[infoIndex] = defaultRadius;
@@ -204,7 +204,7 @@ public class ProximityInfo {
" [%2d] row=%d x/y/r=%7.2f/%7.2f/%5.2f %s code=%s", infoIndex, row,
sweetSpotCenterXs[infoIndex], sweetSpotCenterYs[infoIndex],
sweetSpotRadii[infoIndex], (row < rows ? "correct" : "default"),
- Constants.printableCode(key.mCode)));
+ Constants.printableCode(key.getCode())));
}
infoIndex++;
}
@@ -322,25 +322,29 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get
have to align this on the center of the key. Hence, we don't need a separate value for
bottomPixelWithinThreshold and call this yEnd right away.
*/
- final int topPixelWithinThreshold = key.mY - threshold;
+ final int keyX = key.getX();
+ final int keyY = key.getY();
+ final int topPixelWithinThreshold = keyY - threshold;
final int yDeltaToGrid = topPixelWithinThreshold % mCellHeight;
final int yMiddleOfTopCell = topPixelWithinThreshold - yDeltaToGrid + halfCellHeight;
final int yStart = Math.max(halfCellHeight,
yMiddleOfTopCell + (yDeltaToGrid <= halfCellHeight ? 0 : mCellHeight));
- final int yEnd = Math.min(fullGridHeight, key.mY + key.mHeight + threshold);
+ final int yEnd = Math.min(fullGridHeight, keyY + key.getHeight() + threshold);
- final int leftPixelWithinThreshold = key.mX - threshold;
+ final int leftPixelWithinThreshold = keyX - threshold;
final int xDeltaToGrid = leftPixelWithinThreshold % mCellWidth;
final int xMiddleOfLeftCell = leftPixelWithinThreshold - xDeltaToGrid + halfCellWidth;
final int xStart = Math.max(halfCellWidth,
xMiddleOfLeftCell + (xDeltaToGrid <= halfCellWidth ? 0 : mCellWidth));
- final int xEnd = Math.min(fullGridWidth, key.mX + key.mWidth + threshold);
+ final int xEnd = Math.min(fullGridWidth, keyX + key.getWidth() + threshold);
int baseIndexOfCurrentRow = (yStart / mCellHeight) * mGridWidth + (xStart / mCellWidth);
for (int centerY = yStart; centerY <= yEnd; centerY += mCellHeight) {
int index = baseIndexOfCurrentRow;
for (int centerX = xStart; centerX <= xEnd; centerX += mCellWidth) {
- if (key.squaredDistanceToEdge(centerX, centerY) < thresholdSquared) {
+ // TODO: Remove "index < neighborCountPerCell.length" below.
+ if (index < neighborCountPerCell.length
+ && key.squaredDistanceToEdge(centerX, centerY) < thresholdSquared) {
neighborsFlatBuffer[index * keyCount + neighborCountPerCell[index]] = key;
++neighborCountPerCell[index];
}
@@ -372,7 +376,7 @@ y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get
if (index >= destLength) {
break;
}
- final int code = key.mCode;
+ final int code = key.getCode();
if (code <= Constants.CODE_SPACE) {
break;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
new file mode 100644
index 000000000..c10fdbace
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -0,0 +1,85 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.latin.Constants;
+
+/**
+ * The string parser of codesArray specification for <GridRows />. The attribute codesArray is an
+ * array of string.
+ * Each element of the array defines a key label by specifying a code point as a hexadecimal string.
+ * A key label may consist of multiple code points separated by comma.
+ * Each element of the array optionally can have an output text definition after vertical bar
+ * marker. An output text may consist of multiple code points separated by comma.
+ * The format of the codesArray element should be:
+ * <pre>
+ * codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)?
+ * </pre>
+ */
+// TODO: Write unit tests for this class.
+public final class CodesArrayParser {
+ // Constants for parsing.
+ private static final char COMMA = ',';
+ private static final char VERTICAL_BAR = '|';
+ private static final String COMMA_STRING = ",";
+ private static final int BASE_HEX = 16;
+
+ private CodesArrayParser() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static String getLabelSpec(final String codesArraySpec) {
+ final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+ return (pos < 0) ? codesArraySpec : codesArraySpec.substring(0, pos);
+ }
+
+ public static String parseLabel(final String codesArraySpec) {
+ final String labelSpec = getLabelSpec(codesArraySpec);
+ final StringBuilder sb = new StringBuilder();
+ for (final String codeInHex : labelSpec.split(COMMA_STRING)) {
+ final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+ sb.appendCodePoint(codePoint);
+ }
+ return sb.toString();
+ }
+
+ private static String getCodeSpec(final String codesArraySpec) {
+ final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+ return (pos < 0) ? codesArraySpec : codesArraySpec.substring(pos + 1);
+ }
+
+ public static int parseCode(final String codesArraySpec) {
+ final String codeSpec = getCodeSpec(codesArraySpec);
+ if (codeSpec.indexOf(COMMA) < 0) {
+ return Integer.parseInt(codeSpec, BASE_HEX);
+ }
+ return Constants.CODE_OUTPUT_TEXT;
+ }
+
+ public static String parseOutputText(final String codesArraySpec) {
+ final String codeSpec = getCodeSpec(codesArraySpec);
+ if (codeSpec.indexOf(COMMA) < 0) {
+ return null;
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+ final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+ sb.appendCodePoint(codePoint);
+ }
+ return sb.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index f65056948..e6a674334 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -24,7 +24,7 @@ public abstract class KeyStyle {
public abstract String[] getStringArray(TypedArray a, int index);
public abstract String getString(TypedArray a, int index);
public abstract int getInt(TypedArray a, int index, int defaultValue);
- public abstract int getFlag(TypedArray a, int index);
+ public abstract int getFlags(TypedArray a, int index);
protected KeyStyle(final KeyboardTextsSet textsSet) {
mTextsSet = textsSet;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
index 6aab3e7b3..05d855e31 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
@@ -66,7 +66,7 @@ public final class KeyStylesSet {
}
@Override
- public int getFlag(final TypedArray a, final int index) {
+ public int getFlags(final TypedArray a, final int index) {
return a.getInt(index, 0);
}
}
@@ -123,14 +123,12 @@ public final class KeyStylesSet {
}
@Override
- public int getFlag(final TypedArray a, final int index) {
- int flags = a.getInt(index, 0);
- final Object value = mStyleAttributes.get(index);
- if (value != null) {
- flags |= (Integer)value;
- }
- final KeyStyle parentStyle = mStyles.get(mParentStyleName);
- return flags | parentStyle.getFlag(a, index);
+ public int getFlags(final TypedArray a, final int index) {
+ final int parentFlags = mStyles.get(mParentStyleName).getFlags(a, index);
+ final Integer value = (Integer)mStyleAttributes.get(index);
+ final int styleFlags = (value != null) ? value : 0;
+ final int flags = a.getInt(index, 0);
+ return flags | styleFlags | parentFlags;
}
public void readKeyAttributes(final TypedArray keyAttr) {
@@ -142,13 +140,13 @@ public final class KeyStylesSet {
readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
- readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
+ readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
readString(keyAttr, R.styleable.Keyboard_Key_keyIcon);
readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled);
readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview);
readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn);
readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
- readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
}
private void readString(final TypedArray a, final int index) {
@@ -163,10 +161,11 @@ public final class KeyStylesSet {
}
}
- private void readFlag(final TypedArray a, final int index) {
+ private void readFlags(final TypedArray a, final int index) {
if (a.hasValue(index)) {
final Integer value = (Integer)mStyleAttributes.get(index);
- mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
+ final int styleFlags = value != null ? value : 0;
+ mStyleAttributes.put(index, a.getInt(index, 0) | styleFlags);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index b34d7c45f..22f7a83fc 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -29,6 +29,7 @@ import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.RunInLocale;
@@ -113,6 +114,7 @@ import java.util.Locale;
* </pre>
*/
+// TODO: Write unit tests for this class.
public class KeyboardBuilder<KP extends KeyboardParams> {
private static final String BUILDER_TAG = "Keyboard.Builder";
private static final boolean DEBUG = false;
@@ -120,6 +122,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
// Keyboard XML Tags
private static final String TAG_KEYBOARD = "Keyboard";
private static final String TAG_ROW = "Row";
+ private static final String TAG_GRID_ROWS = "GridRows";
private static final String TAG_KEY = "Key";
private static final String TAG_SPACER = "Spacer";
private static final String TAG_INCLUDE = "include";
@@ -218,20 +221,18 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
parseKeyboardAttributes(parser);
startKeyboard();
parseKeyboardContent(parser, false);
- break;
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
+ return;
}
+ throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
}
}
}
private void parseKeyboardAttributes(final XmlPullParser parser) {
+ final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
- Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
- R.style.Keyboard);
- final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
+ attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
+ final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
final KeyboardParams params = mParams;
final int height = params.mId.mHeight;
@@ -314,6 +315,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
startRow(row);
}
parseRowContent(parser, row, skip);
+ } else if (TAG_GRID_ROWS.equals(tag)) {
+ if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : "");
+ parseGridRows(parser, skip);
} else if (TAG_INCLUDE.equals(tag)) {
parseIncludeKeyboardContent(parser, skip);
} else if (TAG_SWITCH.equals(tag)) {
@@ -328,31 +332,30 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (DEBUG) endTag("</%s>", tag);
if (TAG_KEYBOARD.equals(tag)) {
endKeyboard();
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
+ return;
+ }
+ if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
private KeyboardRow parseRowAttributes(final XmlPullParser parser)
throws XmlPullParserException {
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
try {
- if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
+ if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
}
- if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
+ if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
}
return new KeyboardRow(mResources, mParams, parser, mCurrentY);
} finally {
- a.recycle();
+ keyboardAttr.recycle();
}
}
@@ -382,34 +385,97 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (!skip) {
endRow(row);
}
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
+ return;
+ }
+ if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
- private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ private void parseGridRows(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
if (skip) {
- XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
if (DEBUG) {
- startEndTag("<%s /> skipped", TAG_KEY);
+ startEndTag("<%s /> skipped", TAG_GRID_ROWS);
}
- } else {
- final Key key = new Key(mResources, mParams, row, parser);
- if (DEBUG) {
- startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
- (key.isEnabled() ? "" : " disabled"), key,
- Arrays.toString(key.mMoreKeys));
+ return;
+ }
+ final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+ final TypedArray gridRowAttr = mResources.obtainAttributes(
+ Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows);
+ final int codesArrayId = gridRowAttr.getResourceId(
+ R.styleable.Keyboard_GridRows_codesArray, 0);
+ final int textsArrayId = gridRowAttr.getResourceId(
+ R.styleable.Keyboard_GridRows_textsArray, 0);
+ gridRowAttr.recycle();
+ if (codesArrayId == 0 && textsArrayId == 0) {
+ throw new XmlParseUtils.ParseException(
+ "Missing codesArray or textsArray attributes", parser);
+ }
+ if (codesArrayId != 0 && textsArrayId != 0) {
+ throw new XmlParseUtils.ParseException(
+ "Both codesArray and textsArray attributes specifed", parser);
+ }
+ final String[] array = mResources.getStringArray(
+ codesArrayId != 0 ? codesArrayId : textsArrayId);
+ final int counts = array.length;
+ final float keyWidth = gridRows.getKeyWidth(null, 0.0f);
+ final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth);
+ for (int index = 0; index < counts; index += numColumns) {
+ final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+ startRow(row);
+ for (int c = 0; c < numColumns; c++) {
+ final int i = index + c;
+ if (i >= counts) {
+ break;
+ }
+ final String label;
+ final int code;
+ final String outputText;
+ if (codesArrayId != 0) {
+ final String codeArraySpec = array[i];
+ label = CodesArrayParser.parseLabel(codeArraySpec);
+ code = CodesArrayParser.parseCode(codeArraySpec);
+ outputText = CodesArrayParser.parseOutputText(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;
+ }
+ final int x = (int)row.getKeyX(null);
+ final int y = row.getKeyY();
+ final Key key = new Key(mParams, label, null /* hintLabel */, 0 /* iconId */,
+ code, outputText, x, y, (int)keyWidth, (int)row.getRowHeight(),
+ row.getDefaultKeyLabelFlags(), row.getDefaultBackgroundType());
+ endKey(key);
+ row.advanceXPos(keyWidth);
}
+ endRow(row);
+ }
+
+ XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
+ }
+
+ private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
XmlParseUtils.checkEndTag(TAG_KEY, parser);
- endKey(key);
+ if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
+ return;
}
+ final Key key = new Key(mResources, mParams, row, parser);
+ if (DEBUG) {
+ startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"),
+ key, Arrays.toString(key.getMoreKeys()));
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ endKey(key);
}
private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
@@ -417,12 +483,12 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (skip) {
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
- } else {
- final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
- if (DEBUG) startEndTag("<%s />", TAG_SPACER);
- XmlParseUtils.checkEndTag(TAG_SPACER, parser);
- endKey(spacer);
+ return;
}
+ final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
+ if (DEBUG) startEndTag("<%s />", TAG_SPACER);
+ XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+ endKey(spacer);
}
private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
@@ -440,66 +506,44 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (skip) {
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
- } else {
- final AttributeSet attr = Xml.asAttributeSet(parser);
- final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
- R.styleable.Keyboard_Include);
- final TypedArray keyAttr = mResources.obtainAttributes(attr,
- R.styleable.Keyboard_Key);
- int keyboardLayout = 0;
- float savedDefaultKeyWidth = 0;
- int savedDefaultKeyLabelFlags = 0;
- int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
- try {
- XmlParseUtils.checkAttributeExists(keyboardAttr,
- R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
- TAG_INCLUDE, parser);
- keyboardLayout = keyboardAttr.getResourceId(
- R.styleable.Keyboard_Include_keyboardLayout, 0);
- if (row != null) {
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- // Override current x coordinate.
- row.setXPos(row.getKeyX(keyAttr));
- }
- // TODO: Remove this if-clause and do the same as backgroundType below.
- savedDefaultKeyWidth = row.getDefaultKeyWidth();
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
- // Override default key width.
- row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
- }
- savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
- // Bitwise-or default keyLabelFlag if exists.
- row.setDefaultKeyLabelFlags(keyAttr.getInt(
- R.styleable.Keyboard_Key_keyLabelFlags, 0)
- | savedDefaultKeyLabelFlags);
- savedDefaultBackgroundType = row.getDefaultBackgroundType();
- // Override default backgroundType if exists.
- row.setDefaultBackgroundType(keyAttr.getInt(
- R.styleable.Keyboard_Key_backgroundType,
- savedDefaultBackgroundType));
- }
- } finally {
- keyboardAttr.recycle();
- keyAttr.recycle();
+ return;
+ }
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyboardAttr = mResources.obtainAttributes(
+ attr, R.styleable.Keyboard_Include);
+ final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
+ int keyboardLayout = 0;
+ try {
+ XmlParseUtils.checkAttributeExists(
+ keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
+ TAG_INCLUDE, parser);
+ keyboardLayout = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_Include_keyboardLayout, 0);
+ if (row != null) {
+ // Override current x coordinate.
+ row.setXPos(row.getKeyX(keyAttr));
+ // Push current Row attributes and update with new attributes.
+ row.pushRowAttributes(keyAttr);
}
+ } finally {
+ keyboardAttr.recycle();
+ keyAttr.recycle();
+ }
- XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
- if (DEBUG) {
- startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
- mResources.getResourceEntryName(keyboardLayout));
- }
- final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
- try {
- parseMerge(parserForInclude, row, skip);
- } finally {
- if (row != null) {
- // Restore default keyWidth, keyLabelFlags, and backgroundType.
- row.setDefaultKeyWidth(savedDefaultKeyWidth);
- row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
- row.setDefaultBackgroundType(savedDefaultBackgroundType);
- }
- parserForInclude.close();
+ XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+ if (DEBUG) {
+ startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
+ mResources.getResourceEntryName(keyboardLayout));
+ }
+ final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
+ try {
+ parseMerge(parserForInclude, row, skip);
+ } finally {
+ if (row != null) {
+ // Restore Row attributes.
+ row.popRowAttributes();
}
+ parserForInclude.close();
}
}
@@ -516,11 +560,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
} else {
parseRowContent(parser, row, skip);
}
- break;
- } else {
- throw new XmlParseUtils.ParseException(
- "Included keyboard layout must have <merge> root element", parser);
+ return;
}
+ throw new XmlParseUtils.ParseException(
+ "Included keyboard layout must have <merge> root element", parser);
}
}
}
@@ -554,10 +597,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
final String tag = parser.getName();
if (TAG_SWITCH.equals(tag)) {
if (DEBUG) endTag("</%s>", TAG_SWITCH);
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
}
}
}
@@ -580,86 +622,92 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (id == null) {
return true;
}
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Case);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
try {
- final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
+ final boolean keyboardLayoutSetMatched = matchString(caseAttr,
+ R.styleable.Keyboard_Case_keyboardLayoutSet,
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(id.mSubtype));
+ final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
KeyboardId.elementIdToName(id.mElementId));
- final boolean modeMatched = matchTypedValue(a,
+ final boolean modeMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
- final boolean navigateNextMatched = matchBoolean(a,
+ final boolean navigateNextMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
- final boolean navigatePreviousMatched = matchBoolean(a,
+ final boolean navigatePreviousMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
- final boolean passwordInputMatched = matchBoolean(a,
+ final boolean passwordInputMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
- final boolean clobberSettingsKeyMatched = matchBoolean(a,
+ final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
- final boolean shortcutKeyEnabledMatched = matchBoolean(a,
+ final boolean shortcutKeyEnabledMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
- final boolean shortcutKeyOnSymbolsMatched = matchBoolean(a,
+ final boolean shortcutKeyOnSymbolsMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols);
- final boolean hasShortcutKeyMatched = matchBoolean(a,
+ final boolean hasShortcutKeyMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
- final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
+ final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
id.mLanguageSwitchKeyEnabled);
- final boolean isMultiLineMatched = matchBoolean(a,
+ final boolean isMultiLineMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
- final boolean imeActionMatched = matchInteger(a,
+ final boolean imeActionMatched = matchInteger(caseAttr,
R.styleable.Keyboard_Case_imeAction, id.imeAction());
- final boolean localeCodeMatched = matchString(a,
+ final boolean localeCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
- final boolean languageCodeMatched = matchString(a,
+ final boolean languageCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
- final boolean countryCodeMatched = matchString(a,
+ final boolean countryCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
- final boolean selected = keyboardLayoutSetElementMatched && modeMatched
- && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
- && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
- && shortcutKeyOnSymbolsMatched && hasShortcutKeyMatched
- && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched
- && localeCodeMatched && languageCodeMatched && countryCodeMatched;
+ final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
+ && modeMatched && navigateNextMatched && navigatePreviousMatched
+ && passwordInputMatched && clobberSettingsKeyMatched
+ && shortcutKeyEnabledMatched && shortcutKeyOnSymbolsMatched
+ && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
+ && isMultiLineMatched && imeActionMatched && localeCodeMatched
+ && languageCodeMatched && countryCodeMatched;
if (DEBUG) {
- startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
- textAttr(a.getString(
+ startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
+ textAttr(caseAttr.getString(
+ R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"),
+ textAttr(caseAttr.getString(
R.styleable.Keyboard_Case_keyboardLayoutSetElement),
"keyboardLayoutSetElement"),
- textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction),
"imeAction"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext,
"navigateNext"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious,
"navigatePrevious"),
- booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey,
"clobberSettingsKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput,
"passwordInput"),
- booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyEnabled,
"shortcutKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyOnSymbols,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyOnSymbols,
"shortcutKeyOnSymbols"),
- booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey,
"hasShortcutKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
"languageSwitchKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine,
"isMultiLine"),
- textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode),
"localeCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode),
"languageCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode),
"countryCode"),
selected ? "" : " skipped");
}
return selected;
} finally {
- a.recycle();
+ caseAttr.recycle();
}
}
@@ -692,7 +740,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
if (ResourceUtils.isIntegerValue(v)) {
return intValue == a.getInt(index, 0);
- } else if (ResourceUtils.isStringValue(v)) {
+ }
+ if (ResourceUtils.isStringValue(v)) {
return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
}
return false;
@@ -711,10 +760,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
- TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_KeyStyle);
- TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyStyleAttr = mResources.obtainAttributes(
+ attr, R.styleable.Keyboard_KeyStyle);
+ final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
@@ -756,7 +805,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
mRightEdgeKey = null;
}
addEdgeSpace(mParams.mRightPadding, row);
- mCurrentY += row.mRowHeight;
+ mCurrentY += row.getRowHeight();
mCurrentRow = null;
mTopEdge = false;
}
@@ -774,7 +823,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
private void endKeyboard() {
- // nothing to do here.
+ // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
+ // previously expected.
+ final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
+ mParams.mOccupiedHeight = Math.max(mParams.mOccupiedHeight, actualHeight);
}
private void addEdgeSpace(final float width, final KeyboardRow row) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
index a57b83ac0..d32bb7581 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -85,7 +85,7 @@ public class KeyboardParams {
public void onAddKey(final Key newKey) {
final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
final boolean isSpacer = key.isSpacer();
- if (isSpacer && key.mWidth == 0) {
+ if (isSpacer && key.getWidth() == 0) {
// Ignore zero width {@link Spacer}.
return;
}
@@ -94,7 +94,7 @@ public class KeyboardParams {
return;
}
updateHistogram(key);
- if (key.mCode == Constants.CODE_SHIFT) {
+ if (key.getCode() == Constants.CODE_SHIFT) {
mShiftKeys.add(key);
}
if (key.altCodeWhileTyping()) {
@@ -125,14 +125,14 @@ public class KeyboardParams {
}
private void updateHistogram(final Key key) {
- final int height = key.mHeight + mVerticalGap;
+ final int height = key.getHeight() + mVerticalGap;
final int heightCount = updateHistogramCounter(mHeightHistogram, height);
if (heightCount > mMaxHeightCount) {
mMaxHeightCount = heightCount;
mMostCommonKeyHeight = height;
}
- final int width = key.mWidth + mHorizontalGap;
+ final int width = key.getWidth() + mHorizontalGap;
final int widthCount = updateHistogramCounter(mWidthHistogram, width);
if (widthCount > mMaxWidthCount) {
mMaxWidthCount = widthCount;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
index 5fe84a704..0f9497c27 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -23,10 +23,13 @@ import android.util.Xml;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import org.xmlpull.v1.XmlPullParser;
+import java.util.ArrayDeque;
+
/**
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
* Some of the key size defaults can be overridden per row from what the {@link Keyboard}
@@ -38,64 +41,100 @@ public final class KeyboardRow {
private static final int KEYWIDTH_FILL_RIGHT = -1;
private final KeyboardParams mParams;
- /** Default width of a key in this row. */
- private float mDefaultKeyWidth;
- /** Default height of a key in this row. */
- public final int mRowHeight;
- /** Default keyLabelFlags in this row. */
- private int mDefaultKeyLabelFlags;
- /** Default backgroundType for this row */
- private int mDefaultBackgroundType;
+ /** The height of this row. */
+ private final int mRowHeight;
+
+ private final ArrayDeque<RowAttributes> mRowAttributesStack = CollectionUtils.newArrayDeque();
+
+ private static class RowAttributes {
+ /** Default width of a key in this row. */
+ public final float mDefaultKeyWidth;
+ /** Default keyLabelFlags in this row. */
+ public final int mDefaultKeyLabelFlags;
+ /** Default backgroundType for this row */
+ public final int mDefaultBackgroundType;
+
+ /**
+ * Parse and create key attributes. This constructor is used to parse Row tag.
+ *
+ * @param keyAttr an attributes array of Row tag.
+ * @param defaultKeyWidth a default key width.
+ * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+ */
+ public RowAttributes(final TypedArray keyAttr, final float defaultKeyWidth,
+ final int keyboardWidth) {
+ mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ keyboardWidth, keyboardWidth, defaultKeyWidth);
+ mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ Key.BACKGROUND_TYPE_NORMAL);
+ }
+
+ /**
+ * Parse and update key attributes using default attributes. This constructor is used
+ * to parse include tag.
+ *
+ * @param keyAttr an attributes array of include tag.
+ * @param defaultRowAttr default Row attributes.
+ * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+ */
+ public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr,
+ final int keyboardWidth) {
+ mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ keyboardWidth, keyboardWidth, defaultRowAttr.mDefaultKeyWidth);
+ mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0)
+ | defaultRowAttr.mDefaultKeyLabelFlags;
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ defaultRowAttr.mDefaultBackgroundType);
+ }
+ }
private final int mCurrentY;
// Will be updated by {@link Key}'s constructor.
private float mCurrentX;
- public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
- final int y) {
+ public KeyboardRow(final Resources res, final KeyboardParams params,
+ final XmlPullParser parser, final int y) {
mParams = params;
final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard);
mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight,
- params.mBaseHeight, params.mDefaultRowHeight);
+ R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
keyboardAttr.recycle();
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
- mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
- params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth);
- mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
- Key.BACKGROUND_TYPE_NORMAL);
+ mRowAttributesStack.push(new RowAttributes(
+ keyAttr, params.mDefaultKeyWidth, params.mBaseWidth));
keyAttr.recycle();
- // TODO: Initialize this with <Row> attribute as backgroundType is done.
- mDefaultKeyLabelFlags = 0;
mCurrentY = y;
mCurrentX = 0.0f;
}
- public float getDefaultKeyWidth() {
- return mDefaultKeyWidth;
+ public int getRowHeight() {
+ return mRowHeight;
}
- public void setDefaultKeyWidth(final float defaultKeyWidth) {
- mDefaultKeyWidth = defaultKeyWidth;
+ public void pushRowAttributes(final TypedArray keyAttr) {
+ final RowAttributes newAttributes = new RowAttributes(
+ keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth);
+ mRowAttributesStack.push(newAttributes);
}
- public int getDefaultKeyLabelFlags() {
- return mDefaultKeyLabelFlags;
+ public void popRowAttributes() {
+ mRowAttributesStack.pop();
}
- public void setDefaultKeyLabelFlags(final int keyLabelFlags) {
- mDefaultKeyLabelFlags = keyLabelFlags;
+ public float getDefaultKeyWidth() {
+ return mRowAttributesStack.peek().mDefaultKeyWidth;
}
- public int getDefaultBackgroundType() {
- return mDefaultBackgroundType;
+ public int getDefaultKeyLabelFlags() {
+ return mRowAttributesStack.peek().mDefaultKeyLabelFlags;
}
- public void setDefaultBackgroundType(final int backgroundType) {
- mDefaultBackgroundType = backgroundType;
+ public int getDefaultBackgroundType() {
+ return mRowAttributesStack.peek().mDefaultBackgroundType;
}
public void setXPos(final float keyXPos) {
@@ -111,29 +150,27 @@ public final class KeyboardRow {
}
public float getKeyX(final TypedArray keyAttr) {
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
- mParams.mBaseWidth, mParams.mBaseWidth, 0);
- if (keyXPos < 0) {
- // If keyXPos is negative, the actual x-coordinate will be
- // keyboardWidth + keyXPos.
- // keyXPos shouldn't be less than mCurrentX because drawable area for this
- // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
- // its left hand side.
- final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
- return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
- } else {
- return keyXPos + mParams.mLeftPadding;
- }
+ if (keyAttr == null || !keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+ return mCurrentX;
}
- return mCurrentX;
- }
-
- public float getKeyWidth(final TypedArray keyAttr) {
- return getKeyWidth(keyAttr, mCurrentX);
+ final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
+ mParams.mBaseWidth, mParams.mBaseWidth, 0);
+ if (keyXPos >= 0) {
+ return keyXPos + mParams.mLeftPadding;
+ }
+ // If keyXPos is negative, the actual x-coordinate will be
+ // keyboardWidth + keyXPos.
+ // keyXPos shouldn't be less than mCurrentX because drawable area for this
+ // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
+ // its left hand side.
+ final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
+ return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
}
public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) {
+ if (keyAttr == null) {
+ return getDefaultKeyWidth();
+ }
final int widthType = ResourceUtils.getEnumValue(keyAttr,
R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
switch (widthType) {
@@ -144,7 +181,7 @@ public final class KeyboardRow {
return keyboardRightEdge - keyXPos;
default: // KEYWIDTH_NOT_ENUM
return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
- mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth);
+ mParams.mBaseWidth, mParams.mBaseWidth, getDefaultKeyWidth());
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 164910dd4..089db12a2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -46,7 +46,7 @@ public final class KeyboardState {
public void setAlphabetShiftLockedKeyboard();
public void setAlphabetShiftLockShiftedKeyboard();
public void setSymbolsKeyboard();
- public void setSymbolsShiftedKeyboard();
+ public void setEmojiKeyboard();
/**
* Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}.
@@ -64,21 +64,20 @@ public final class KeyboardState {
private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
// TODO: Merge {@link #mSwitchState}, {@link #mIsAlphabetMode}, {@link #mAlphabetShiftState},
- // {@link #mIsSymbolShifted}, {@link #mPrevMainKeyboardWasShiftLocked}, and
- // {@link #mPrevSymbolsKeyboardWasShifted} into single state variable.
+ // {@link #mPrevMainKeyboardWasShiftLocked} into single state variable.
private static final int SWITCH_STATE_ALPHA = 0;
private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
private static final int SWITCH_STATE_SYMBOL = 2;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
- private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
private static final int SWITCH_STATE_MOMENTARY_ALPHA_SHIFT = 5;
private int mSwitchState = SWITCH_STATE_ALPHA;
+ // TODO: Consolidate these two mode booleans into one integer to distinguish between alphabet,
+ // symbols, and emoji mode.
private boolean mIsAlphabetMode;
+ private boolean mIsEmojiMode;
private AlphabetShiftState mAlphabetShiftState = new AlphabetShiftState();
- private boolean mIsSymbolShifted;
private boolean mPrevMainKeyboardWasShiftLocked;
- private boolean mPrevSymbolsKeyboardWasShifted;
private int mRecapitalizeMode;
// For handling double tap.
@@ -91,6 +90,7 @@ public final class KeyboardState {
public boolean mIsValid;
public boolean mIsAlphabetMode;
public boolean mIsAlphabetShiftLocked;
+ public boolean mIsEmojiMode;
public int mShiftMode;
@Override
@@ -99,8 +99,10 @@ public final class KeyboardState {
if (mIsAlphabetMode) {
if (mIsAlphabetShiftLocked) return "ALPHABET_SHIFT_LOCKED";
return "ALPHABET_" + shiftModeToString(mShiftMode);
+ } else if (mIsEmojiMode) {
+ return "EMOJI";
} else {
- return "SYMBOLS_" + shiftModeToString(mShiftMode);
+ return "SYMBOLS";
}
}
}
@@ -117,7 +119,6 @@ public final class KeyboardState {
// Reset alphabet shift state.
mAlphabetShiftState.setShiftLocked(false);
mPrevMainKeyboardWasShiftLocked = false;
- mPrevSymbolsKeyboardWasShifted = false;
mShiftKeyState.onRelease();
mSymbolKeyState.onRelease();
onRestoreKeyboardState();
@@ -131,13 +132,13 @@ public final class KeyboardState {
public void onSaveKeyboardState() {
final SavedKeyboardState state = mSavedKeyboardState;
state.mIsAlphabetMode = mIsAlphabetMode;
+ state.mIsEmojiMode = mIsEmojiMode;
if (mIsAlphabetMode) {
state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
state.mShiftMode = mAlphabetShiftState.isAutomaticShifted() ? AUTOMATIC_SHIFT
: (mAlphabetShiftState.isShiftedOrShiftLocked() ? MANUAL_SHIFT : UNSHIFT);
} else {
state.mIsAlphabetShiftLocked = mPrevMainKeyboardWasShiftLocked;
- state.mShiftMode = mIsSymbolShifted ? MANUAL_SHIFT : UNSHIFT;
}
state.mIsValid = true;
if (DEBUG_EVENT) {
@@ -152,12 +153,10 @@ public final class KeyboardState {
}
if (!state.mIsValid || state.mIsAlphabetMode) {
setAlphabetKeyboard();
+ } else if (state.mIsEmojiMode) {
+ setEmojiKeyboard();
} else {
- if (state.mShiftMode == MANUAL_SHIFT) {
- setSymbolsShiftedKeyboard();
- } else {
- setSymbolsKeyboard();
- }
+ setSymbolsKeyboard();
}
if (!state.mIsValid) return;
@@ -233,14 +232,8 @@ public final class KeyboardState {
}
if (mIsAlphabetMode) {
mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
- if (mPrevSymbolsKeyboardWasShifted) {
- setSymbolsShiftedKeyboard();
- } else {
- setSymbolsKeyboard();
- }
- mPrevSymbolsKeyboardWasShifted = false;
+ setSymbolsKeyboard();
} else {
- mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
setAlphabetKeyboard();
if (mPrevMainKeyboardWasShiftLocked) {
setShiftLocked(true);
@@ -257,7 +250,6 @@ public final class KeyboardState {
}
if (mIsAlphabetMode) return;
- mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
setAlphabetKeyboard();
if (mPrevMainKeyboardWasShiftLocked) {
setShiftLocked(true);
@@ -265,14 +257,6 @@ public final class KeyboardState {
mPrevMainKeyboardWasShiftLocked = false;
}
- private void toggleShiftInSymbols() {
- if (mIsSymbolShifted) {
- setSymbolsKeyboard();
- } else {
- setSymbolsShiftedKeyboard();
- }
- }
-
private void setAlphabetKeyboard() {
if (DEBUG_ACTION) {
Log.d(TAG, "setAlphabetKeyboard");
@@ -280,7 +264,7 @@ public final class KeyboardState {
mSwitchActions.setAlphabetKeyboard();
mIsAlphabetMode = true;
- mIsSymbolShifted = false;
+ mIsEmojiMode = false;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
mSwitchState = SWITCH_STATE_ALPHA;
mSwitchActions.requestUpdatingShiftState();
@@ -292,22 +276,18 @@ public final class KeyboardState {
}
mSwitchActions.setSymbolsKeyboard();
mIsAlphabetMode = false;
- mIsSymbolShifted = false;
// Reset alphabet shift state.
mAlphabetShiftState.setShiftLocked(false);
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
- private void setSymbolsShiftedKeyboard() {
+ private void setEmojiKeyboard() {
if (DEBUG_ACTION) {
- Log.d(TAG, "setSymbolsShiftedKeyboard");
+ Log.d(TAG, "setEmojiKeyboard");
}
- mSwitchActions.setSymbolsShiftedKeyboard();
mIsAlphabetMode = false;
- mIsSymbolShifted = true;
- // Reset alphabet shift state.
- mAlphabetShiftState.setShiftLocked(false);
- mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ mIsEmojiMode = true;
+ mSwitchActions.setEmojiKeyboard();
}
public void onPressKey(final int code, final boolean isSinglePointer, final int autoCaps) {
@@ -357,7 +337,7 @@ public final class KeyboardState {
} else if (code == Constants.CODE_CAPSLOCK) {
setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
- onReleaseSymbol(withSliding);
+ onReleaseSymbol();
}
}
@@ -367,16 +347,11 @@ public final class KeyboardState {
mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
}
- private void onReleaseSymbol(final boolean withSliding) {
+ private void onReleaseSymbol() {
if (mSymbolKeyState.isChording()) {
// Switch back to the previous keyboard mode if the user chords the mode change key and
// another key, then releases the mode change key.
toggleAlphabetAndSymbols();
- } else if (!withSliding) {
- // If the mode change key is being released without sliding, we should forget the
- // previous symbols keyboard shift state and simply switch back to symbols layout
- // (never symbols shifted) next time the mode gets changed to symbols layout.
- mPrevSymbolsKeyboardWasShifted = false;
}
mSymbolKeyState.onRelease();
}
@@ -442,47 +417,43 @@ public final class KeyboardState {
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
return;
}
- if (mIsAlphabetMode) {
- mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapShiftKeyTimeout();
- if (!mIsInDoubleTapShiftKey) {
- // This is first tap.
- mSwitchActions.startDoubleTapShiftKeyTimer();
- }
- if (mIsInDoubleTapShiftKey) {
- if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
- // Shift key has been double tapped while in manual shifted or automatic
- // shifted state.
- setShiftLocked(true);
- } else {
- // Shift key has been double tapped while in normal state. This is the second
- // tap to disable shift locked state, so just ignore this.
- }
+ if (!mIsAlphabetMode) {
+ // There is no shift key on symbols keyboard.
+ return;
+ }
+ mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapShiftKeyTimeout();
+ if (!mIsInDoubleTapShiftKey) {
+ // This is first tap.
+ mSwitchActions.startDoubleTapShiftKeyTimer();
+ }
+ if (mIsInDoubleTapShiftKey) {
+ if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
+ // Shift key has been double tapped while in manual shifted or automatic
+ // shifted state.
+ setShiftLocked(true);
} else {
- if (mAlphabetShiftState.isShiftLocked()) {
- // Shift key is pressed while shift locked state, we will treat this state as
- // shift lock shifted state and mark as if shift key pressed while normal state.
- setShifted(SHIFT_LOCK_SHIFTED);
- mShiftKeyState.onPress();
- } else if (mAlphabetShiftState.isAutomaticShifted()) {
- // Shift key is pressed while automatic shifted, we have to move to manual
- // shifted.
- setShifted(MANUAL_SHIFT);
- mShiftKeyState.onPress();
- } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
- // In manual shifted state, we just record shift key has been pressing while
- // shifted state.
- mShiftKeyState.onPressOnShifted();
- } else {
- // In base layout, chording or manual shifted mode is started.
- setShifted(MANUAL_SHIFT);
- mShiftKeyState.onPress();
- }
+ // Shift key has been double tapped while in normal state. This is the second
+ // tap to disable shift locked state, so just ignore this.
}
} else {
- // In symbol mode, just toggle symbol and symbol more keyboard.
- toggleShiftInSymbols();
- mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
- mShiftKeyState.onPress();
+ if (mAlphabetShiftState.isShiftLocked()) {
+ // Shift key is pressed while shift locked state, we will treat this state as
+ // shift lock shifted state and mark as if shift key pressed while normal state.
+ setShifted(SHIFT_LOCK_SHIFTED);
+ mShiftKeyState.onPress();
+ } else if (mAlphabetShiftState.isAutomaticShifted()) {
+ // Shift key is pressed while automatic shifted, we have to move to manual shifted.
+ setShifted(MANUAL_SHIFT);
+ mShiftKeyState.onPress();
+ } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
+ // In manual shifted state, we just record shift key has been pressing while
+ // shifted state.
+ mShiftKeyState.onPressOnShifted();
+ } else {
+ // In base layout, chording or manual shifted mode is started.
+ setShifted(MANUAL_SHIFT);
+ mShiftKeyState.onPress();
+ }
}
}
@@ -537,11 +508,7 @@ public final class KeyboardState {
mIsInAlphabetUnshiftedFromShifted = true;
}
} else {
- // In symbol mode, switch back to the previous keyboard mode if the user chords the
- // shift key and another key, then releases the shift key.
- if (mShiftKeyState.isChording()) {
- toggleShiftInSymbols();
- }
+ // There is no shift key on symbols keyboard.
}
mShiftKeyState.onRelease();
}
@@ -555,9 +522,6 @@ public final class KeyboardState {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
toggleAlphabetAndSymbols();
break;
- case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
- toggleShiftInSymbols();
- break;
case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT:
setAlphabetKeyboard();
break;
@@ -585,12 +549,6 @@ public final class KeyboardState {
}
}
break;
- case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
- if (code == Constants.CODE_SHIFT) {
- // Detected only the shift key has been pressed on symbol layout, and then released.
- mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
- }
- break;
case SWITCH_STATE_SYMBOL_BEGIN:
if (!isSpaceCharacter(code) && (Constants.isLetterCode(code)
|| code == Constants.CODE_OUTPUT_TEXT)) {
@@ -602,7 +560,6 @@ public final class KeyboardState {
// characters followed by a space/enter.
if (isSpaceCharacter(code)) {
toggleAlphabetAndSymbols();
- mPrevSymbolsKeyboardWasShifted = false;
}
break;
}
@@ -610,6 +567,8 @@ public final class KeyboardState {
// If the code is a letter, update keyboard shift state.
if (Constants.isLetterCode(code)) {
updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
+ } else if (code == Constants.CODE_EMOJI) {
+ setEmojiKeyboard();
}
}
@@ -628,7 +587,6 @@ public final class KeyboardState {
case SWITCH_STATE_SYMBOL_BEGIN: return "SYMBOL-BEGIN";
case SWITCH_STATE_SYMBOL: return "SYMBOL";
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
- case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT: return "MOMENTARY-ALPHA_SHIFT";
default: return null;
}
@@ -636,8 +594,7 @@ public final class KeyboardState {
@Override
public String toString() {
- return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
- : (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
+ return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString() : "SYMBOLS")
+ " shift=" + mShiftKeyState
+ " symbol=" + mSymbolKeyState
+ " switch=" + switchStateToString(mSwitchState) + "]";
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index b55e19d52..6d053f5c0 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -27,18 +27,18 @@ import java.util.HashMap;
/**
* !!!!! DO NOT EDIT THIS FILE !!!!!
*
- * This file is generated by tools/maketext. The base template file is
- * tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+ * This file is generated by tools/make-keyboard-text. The base template file is
+ * tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
*
* This file must be updated when any text resources in keyboard layout files have been changed.
* These text resources are referred as "!text/<resource_name>" in keyboard XML definitions,
* and should be defined in
- * tools/maketext/res/values-<locale>/donottranslate-more-keys.xml
+ * tools/make-keyboard-text/res/values-<locale>/donottranslate-more-keys.xml
*
* To update this file, please run the following commands.
* $ cd $ANDROID_BUILD_TOP
- * $ mmm packages/inputmethods/LatinIME/tools/maketext
- * $ maketext -java packages/inputmethods/LatinIME/java/src
+ * $ mmm packages/inputmethods/LatinIME/tools/make-keyboard-text
+ * $ make-keyboard-text -java packages/inputmethods/LatinIME/java/src
*
* The updated source file will be generated to the following path (this file).
* packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/
@@ -153,8 +153,8 @@ public final class KeyboardTextsSet {
/* 48 */ "single_angle_quotes",
/* 49 */ "double_angle_quotes",
/* 50 */ "more_keys_for_currency_dollar",
- /* 51 */ "keylabel_for_currency_generic",
- /* 52 */ "more_keys_for_currency_generic",
+ /* 51 */ "keylabel_for_currency",
+ /* 52 */ "more_keys_for_currency",
/* 53 */ "more_keys_for_punctuation",
/* 54 */ "more_keys_for_star",
/* 55 */ "more_keys_for_bullet",
@@ -233,25 +233,24 @@ public final class KeyboardTextsSet {
/* 128 */ "label_to_phone_symbols_key",
/* 129 */ "label_time_am",
/* 130 */ "label_time_pm",
- /* 131 */ "label_to_symbol_key_pcqwerty",
- /* 132 */ "keylabel_for_popular_domain",
- /* 133 */ "more_keys_for_popular_domain",
- /* 134 */ "more_keys_for_smiley",
- /* 135 */ "single_laqm_raqm",
- /* 136 */ "single_laqm_raqm_rtl",
- /* 137 */ "single_raqm_laqm",
- /* 138 */ "double_laqm_raqm",
- /* 139 */ "double_laqm_raqm_rtl",
- /* 140 */ "double_raqm_laqm",
- /* 141 */ "single_lqm_rqm",
- /* 142 */ "single_9qm_lqm",
- /* 143 */ "single_9qm_rqm",
- /* 144 */ "double_lqm_rqm",
- /* 145 */ "double_9qm_lqm",
- /* 146 */ "double_9qm_rqm",
- /* 147 */ "more_keys_for_single_quote",
- /* 148 */ "more_keys_for_double_quote",
- /* 149 */ "more_keys_for_tablet_double_quote",
+ /* 131 */ "keylabel_for_popular_domain",
+ /* 132 */ "more_keys_for_popular_domain",
+ /* 133 */ "more_keys_for_smiley",
+ /* 134 */ "single_laqm_raqm",
+ /* 135 */ "single_laqm_raqm_rtl",
+ /* 136 */ "single_raqm_laqm",
+ /* 137 */ "double_laqm_raqm",
+ /* 138 */ "double_laqm_raqm_rtl",
+ /* 139 */ "double_raqm_laqm",
+ /* 140 */ "single_lqm_rqm",
+ /* 141 */ "single_9qm_lqm",
+ /* 142 */ "single_9qm_rqm",
+ /* 143 */ "double_lqm_rqm",
+ /* 144 */ "double_9qm_lqm",
+ /* 145 */ "double_9qm_rqm",
+ /* 146 */ "more_keys_for_single_quote",
+ /* 147 */ "more_keys_for_double_quote",
+ /* 148 */ "more_keys_for_tablet_double_quote",
};
private static final String EMPTY = "";
@@ -278,7 +277,7 @@ public final class KeyboardTextsSet {
/* 50 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
/* 51 */ "$",
/* 52 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1",
- /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+ /* 53 */ "!fixedColumnOrder!3,!,\\,,?,:,;,@",
// U+2020: "†" DAGGER
// U+2021: "‡" DOUBLE DAGGER
// U+2605: "★" BLACK STAR
@@ -289,8 +288,7 @@ public final class KeyboardTextsSet {
// U+2666: "♦" BLACK DIAMOND SUIT
// U+2663: "♣" BLACK CLUB SUIT
/* 55 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
- // U+00B1: "±" PLUS-MINUS SIGN
- /* 56 */ "\u00B1",
+ /* 56 */ EMPTY,
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
/* 57 */ "!fixedColumnOrder!3,<,{,[",
@@ -361,10 +359,9 @@ public final class KeyboardTextsSet {
// U+2030: "‰" PER MILLE SIGN
/* 103 */ "\u2030",
/* 104 */ ",",
- /* 105 */ "!",
- /* 106 */ "!",
- /* 107 */ "?",
- /* 108 */ "?",
+ /* 105~ */
+ EMPTY, EMPTY, EMPTY, EMPTY,
+ /* ~108 */
/* 109 */ "\'",
/* 110 */ "\"",
/* 111 */ "\"",
@@ -396,12 +393,10 @@ public final class KeyboardTextsSet {
/* 129 */ "AM",
// Key label for "post meridiem"
/* 130 */ "PM",
- // Label for "switch to symbols" key on PC QWERTY layout
- /* 131 */ "Sym",
- /* 132 */ ".com",
+ /* 131 */ ".com",
// popular web domains for the locale - most popular, displayed on the keyboard
- /* 133 */ "!hasLabels!,.net,.org,.gov,.edu",
- /* 134 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+ /* 132 */ "!hasLabels!,.net,.org,.gov,.edu",
+ /* 133 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
// U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
// U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
// U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
@@ -423,24 +418,24 @@ public final class KeyboardTextsSet {
// The following each quotation mark pair consist of
// <opening quotation mark>, <closing quotation mark>
// and is named after (single|double)_<opening quotation mark>_<closing quotation mark>.
- /* 135 */ "\u2039,\u203A",
- /* 136 */ "\u2039|\u203A,\u203A|\u2039",
- /* 137 */ "\u203A,\u2039",
- /* 138 */ "\u00AB,\u00BB",
- /* 139 */ "\u00AB|\u00BB,\u00BB|\u00AB",
- /* 140 */ "\u00BB,\u00AB",
+ /* 134 */ "\u2039,\u203A",
+ /* 135 */ "\u2039|\u203A,\u203A|\u2039",
+ /* 136 */ "\u203A,\u2039",
+ /* 137 */ "\u00AB,\u00BB",
+ /* 138 */ "\u00AB|\u00BB,\u00BB|\u00AB",
+ /* 139 */ "\u00BB,\u00AB",
// The following each quotation mark triplet consists of
// <another quotation mark>, <opening quotation mark>, <closing quotation mark>
// and is named after (single|double)_<opening quotation mark>_<closing quotation mark>.
- /* 141 */ "\u201A,\u2018,\u2019",
- /* 142 */ "\u2019,\u201A,\u2018",
- /* 143 */ "\u2018,\u201A,\u2019",
- /* 144 */ "\u201E,\u201C,\u201D",
- /* 145 */ "\u201D,\u201E,\u201C",
- /* 146 */ "\u201C,\u201E,\u201D",
- /* 147 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes",
- /* 148 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes",
- /* 149 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes",
+ /* 140 */ "\u201A,\u2018,\u2019",
+ /* 141 */ "\u2019,\u201A,\u2018",
+ /* 142 */ "\u2018,\u201A,\u2019",
+ /* 143 */ "\u201E,\u201C,\u201D",
+ /* 144 */ "\u201D,\u201E,\u201C",
+ /* 145 */ "\u201C,\u201E,\u201D",
+ /* 146 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes",
+ /* 147 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes",
+ /* 148 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes",
};
/* Language af: Afrikaans */
@@ -519,7 +514,7 @@ public final class KeyboardTextsSet {
// U+061F: "ØŸ" ARABIC QUESTION MARK
// U+060C: "،" ARABIC COMMA
// U+061B: "Ø›" ARABIC SEMICOLON
- /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(",
// U+2605: "★" BLACK STAR
// U+066D: "Ù­" ARABIC FIVE POINTED STAR
/* 54 */ "\u2605,\u066D",
@@ -785,7 +780,7 @@ public final class KeyboardTextsSet {
null, null, null, null, null, null, null, null,
/* ~52 */
// U+00B7: "·" MIDDLE DOT
- /* 53 */ "!fixedColumnOrder!9,\u00B7,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+ /* 53 */ "!fixedColumnOrder!4,\u00B7,!,\\,,?,:,;,@",
/* 54~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
@@ -1257,7 +1252,7 @@ public final class KeyboardTextsSet {
/* ~52 */
// U+00A1: "¡" INVERTED EXCLAMATION MARK
// U+00BF: "¿" INVERTED QUESTION MARK
- /* 53 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)",
+ /* 53 */ "!fixedColumnOrder!4,;,!,\\,,?,:,\u00A1,@,\u00BF",
/* 54~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
@@ -1402,13 +1397,14 @@ public final class KeyboardTextsSet {
/* 47 */ null,
/* 48 */ "!text/single_laqm_raqm_rtl",
/* 49 */ "!text/double_laqm_raqm_rtl",
- /* 50~ */
- null, null, null,
- /* ~52 */
+ /* 50 */ null,
+ // U+FDFC: "ï·¼" RIAL SIGN
+ /* 51 */ "\uFDFC",
+ /* 52 */ null,
// U+061F: "ØŸ" ARABIC QUESTION MARK
// U+060C: "،" ARABIC COMMA
// U+061B: "Ø›" ARABIC SEMICOLON
- /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(",
// U+2605: "★" BLACK STAR
// U+066D: "Ù­" ARABIC FIVE POINTED STAR
/* 54 */ "\u2605,\u066D",
@@ -1762,6 +1758,25 @@ public final class KeyboardTextsSet {
/* 49 */ "!text/double_raqm_laqm",
};
+ /* Language hy: Armenian */
+ private static final String[] LANGUAGE_hy = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ /* ~52 */
+ // U+055E: "Õž" ARMENIAN QUESTION MARK
+ /* 53 */ "!fixedColumnOrder!4,\u055E,!,\\,,?,:,;,@",
+ /* 54~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null,
+ /* ~107 */
+ /* 108 */ "\u055E,?",
+ };
+
/* Language is: Icelandic */
private static final String[] LANGUAGE_is = {
// U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
@@ -1899,15 +1914,16 @@ public final class KeyboardTextsSet {
/* 47 */ "\u201C,\u201D,\u201E",
/* 48 */ "!text/single_laqm_raqm_rtl",
/* 49 */ "!text/double_laqm_raqm_rtl",
- /* 50~ */
- null, null, null, null,
- /* ~53 */
+ /* 50 */ null,
+ // U+20AA: "₪" NEW SHEQEL SIGN
+ /* 51 */ "\u20AA",
+ /* 52 */ null,
+ /* 53 */ null,
// U+2605: "★" BLACK STAR
/* 54 */ "\u2605",
/* 55 */ null,
- // U+00B1: "±" PLUS-MINUS SIGN
// U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN
- /* 56 */ "\u00B1,\uFB29",
+ /* 56 */ "\uFB29",
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
/* 57 */ "!fixedColumnOrder!3,<|>,{|},[|]",
@@ -1920,6 +1936,15 @@ public final class KeyboardTextsSet {
// U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
/* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
/* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ /* 61~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~104 */
+ /* 105 */ "!",
+ /* 106 */ "!",
+ /* 107 */ "?",
+ /* 108 */ "?",
};
/* Language ka: Georgian */
@@ -3296,6 +3321,7 @@ public final class KeyboardTextsSet {
"hi", LANGUAGE_hi, /* Hindi */
"hr", LANGUAGE_hr, /* Croatian */
"hu", LANGUAGE_hu, /* Hungarian */
+ "hy", LANGUAGE_hy, /* Armenian */
"is", LANGUAGE_is, /* Icelandic */
"it", LANGUAGE_it, /* Italian */
"iw", LANGUAGE_iw, /* Hebrew */
diff --git a/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java
new file mode 100644
index 000000000..629c60460
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java
@@ -0,0 +1,157 @@
+/*
+ * 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.keyboard.internal;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.util.ArrayDeque;
+import java.util.Random;
+
+/**
+ * This is a Keyboard class to host recently used keys.
+ */
+// TODO: Save/restore recent keys from/to preferences.
+public class RecentsKeyboard extends Keyboard {
+ private static final int TEMPLATE_KEY_CODE_0 = 0x30;
+ private static final int TEMPLATE_KEY_CODE_1 = 0x31;
+
+ private final int mLeftPadding;
+ private final int mHorizontalStep;
+ private final int mVerticalStep;
+ private final int mColumnsNum;
+ private final int mMaxRecentKeyCount;
+ private final ArrayDeque<RecentKey> mRecentKeys = CollectionUtils.newArrayDeque();
+
+ private Key[] mCachedRecentKeys;
+
+ public RecentsKeyboard(final Keyboard templateKeyboard) {
+ super(templateKeyboard);
+ final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
+ final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
+ mLeftPadding = key0.getX();
+ mHorizontalStep = Math.abs(key1.getX() - key0.getX());
+ mVerticalStep = key0.getHeight() + mVerticalGap;
+ mColumnsNum = mBaseWidth / mHorizontalStep;
+ final int rowsNum = mBaseHeight / mVerticalStep;
+ mMaxRecentKeyCount = mColumnsNum * rowsNum;
+ }
+
+ private Key getTemplateKey(final int code) {
+ for (final Key key : super.getKeys()) {
+ if (key.getCode() == code) {
+ return key;
+ }
+ }
+ throw new RuntimeException("Can't find template key: code=" + code);
+ }
+
+ private final Random random = new Random();
+
+ public void addRecentKey(final Key usedKey) {
+ synchronized (mRecentKeys) {
+ mCachedRecentKeys = null;
+ final RecentKey key = (usedKey instanceof RecentKey)
+ ? (RecentKey)usedKey : new RecentKey(usedKey);
+ while (mRecentKeys.remove(key)) {
+ // Remove duplicate keys.
+ }
+ mRecentKeys.addFirst(key);
+ while (mRecentKeys.size() > mMaxRecentKeyCount) {
+ mRecentKeys.removeLast();
+ }
+ int index = 0;
+ for (final RecentKey recentKey : mRecentKeys) {
+ final int keyX = getKeyX(index);
+ final int keyY = getKeyY(index);
+ final int x = keyX+random.nextInt(recentKey.getWidth());
+ final int y = keyY+random.nextInt(recentKey.getHeight());
+ recentKey.updateCorrdinates(keyX, keyY);
+ index++;
+ }
+ }
+ }
+
+ private int getKeyX(final int index) {
+ final int column = index % mColumnsNum;
+ return column * mHorizontalStep + mLeftPadding;
+ }
+
+ private int getKeyY(final int index) {
+ final int row = index / mColumnsNum;
+ return row * mVerticalStep + mTopPadding;
+ }
+
+ @Override
+ public Key[] getKeys() {
+ synchronized (mRecentKeys) {
+ if (mCachedRecentKeys != null) {
+ return mCachedRecentKeys;
+ }
+ mCachedRecentKeys = mRecentKeys.toArray(new Key[mRecentKeys.size()]);
+ return mCachedRecentKeys;
+ }
+ }
+
+ @Override
+ public Key[] getNearestKeys(final int x, final int y) {
+ // TODO: Calculate the nearest key index in mRecentKeys from x and y.
+ return getKeys();
+ }
+
+ static final class RecentKey extends Key {
+ private int mCurrentX;
+ private int mCurrentY;
+
+ public RecentKey(final Key originalKey) {
+ super(originalKey);
+ }
+
+ public void updateCorrdinates(final int x, final int y) {
+ mCurrentX = x;
+ mCurrentY = y;
+ getHitBox().offsetTo(x, y);
+ }
+
+ @Override
+ public int getX() {
+ return mCurrentX;
+ }
+
+ @Override
+ public int getY() {
+ return mCurrentY;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!(o instanceof Key)) return false;
+ final Key key = (Key)o;
+ if (getCode() != key.getCode()) return false;
+ if (!TextUtils.equals(getLabel(), key.getLabel())) return false;
+ return TextUtils.equals(getOutputText(), key.getOutputText());
+ }
+
+ @Override
+ public String toString() {
+ return "RecentKey: " + super.toString();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
new file mode 100644
index 000000000..b8ee976e8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
@@ -0,0 +1,212 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.widget.ScrollView;
+import android.widget.Scroller;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.latin.R;
+
+/**
+ * This is an extended {@link KeyboardView} class that hosts a scroll keyboard.
+ * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support.
+ */
+// TODO: Implement key popup preview.
+public final class ScrollKeyboardView extends KeyboardView implements
+ ScrollViewWithNotifier.ScrollListener, GestureDetector.OnGestureListener {
+ private static final boolean PAGINATION = false;
+
+ public interface OnKeyClickListener {
+ public void onKeyClick(Key key);
+ }
+
+ private static final OnKeyClickListener EMPTY_LISTENER = new OnKeyClickListener() {
+ @Override
+ public void onKeyClick(final Key key) {}
+ };
+
+ private OnKeyClickListener mListener = EMPTY_LISTENER;
+ private final KeyDetector mKeyDetector = new KeyDetector(0.0f /*keyHysteresisDistance */);
+ private final GestureDetector mGestureDetector;
+
+ private final Scroller mScroller;
+ private ScrollViewWithNotifier mScrollView;
+
+ public ScrollKeyboardView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.keyboardViewStyle);
+ }
+
+ public ScrollKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ mGestureDetector = new GestureDetector(context, this);
+ mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */);
+ mScroller = new Scroller(context);
+ }
+
+ public void setScrollView(final ScrollViewWithNotifier scrollView) {
+ mScrollView = scrollView;
+ scrollView.setScrollListener(this);
+ }
+
+ private final Runnable mScrollTask = new Runnable() {
+ @Override
+ public void run() {
+ final Scroller scroller = mScroller;
+ final ScrollView scrollView = mScrollView;
+ scroller.computeScrollOffset();
+ scrollView.scrollTo(0, scroller.getCurrY());
+ if (!scroller.isFinished()) {
+ scrollView.post(this);
+ }
+ }
+ };
+
+ // {@link ScrollViewWithNotified#ScrollListener} methods.
+ @Override
+ public void notifyScrollChanged(final int scrollX, final int scrollY, final int oldX,
+ final int oldY) {
+ if (PAGINATION) {
+ mScroller.forceFinished(true /* finished */);
+ mScrollView.removeCallbacks(mScrollTask);
+ final int currentTop = mScrollView.getScrollY();
+ final int pageHeight = getKeyboard().mBaseHeight;
+ final int lastPageNo = currentTop / pageHeight;
+ final int lastPageTop = lastPageNo * pageHeight;
+ final int nextPageNo = lastPageNo + 1;
+ final int nextPageTop = Math.min(nextPageNo * pageHeight, getHeight() - pageHeight);
+ final int scrollTo = (currentTop - lastPageTop) < (nextPageTop - currentTop)
+ ? lastPageTop : nextPageTop;
+ final int deltaY = scrollTo - currentTop;
+ mScroller.startScroll(0, currentTop, 0, deltaY, 300);
+ mScrollView.post(mScrollTask);
+ }
+ }
+
+ @Override
+ public void notifyOverScrolled(final int scrollX, final int scrollY, final boolean clampedX,
+ final boolean clampedY) {
+ releaseCurrentKey();
+ }
+
+ public void setOnKeyClickListener(final OnKeyClickListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setKeyboard(final Keyboard keyboard) {
+ super.setKeyboard(keyboard);
+ mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent e) {
+ if (mGestureDetector.onTouchEvent(e)) {
+ return true;
+ }
+ final Key key = getKey(e);
+ if (key != null && key != mCurrentKey) {
+ releaseCurrentKey();
+ }
+ return true;
+ }
+
+ // {@link GestureDetector#OnGestureListener} methods.
+ private Key mCurrentKey;
+
+ private Key getKey(final MotionEvent e) {
+ final int index = e.getActionIndex();
+ final int x = (int)e.getX(index);
+ final int y = (int)e.getY(index);
+ return mKeyDetector.detectHitKey(x, y);
+ }
+
+ public void releaseCurrentKey() {
+ final Key currentKey = mCurrentKey;
+ if (currentKey == null) {
+ return;
+ }
+ currentKey.onReleased();
+ invalidateKey(currentKey);
+ mCurrentKey = null;
+ }
+
+ @Override
+ public boolean onDown(final MotionEvent e) {
+ final Key key = getKey(e);
+ releaseCurrentKey();
+ mCurrentKey = key;
+ if (key == null) {
+ return false;
+ }
+ // TODO: May call {@link KeyboardActionListener#onPressKey(int,int,boolean)}.
+ key.onPressed();
+ invalidateKey(key);
+ return false;
+ }
+
+ @Override
+ public void onShowPress(final MotionEvent e) {
+ // User feedback is done at {@link #onDown(MotionEvent)}.
+ }
+
+ @Override
+ public boolean onSingleTapUp(final MotionEvent e) {
+ final Key key = getKey(e);
+ releaseCurrentKey();
+ if (key == null) {
+ return false;
+ }
+ // TODO: May call {@link KeyboardActionListener#onReleaseKey(int,boolean)}.
+ key.onReleased();
+ invalidateKey(key);
+ mListener.onKeyClick(key);
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
+ final float distanceY) {
+ releaseCurrentKey();
+ return false;
+ }
+
+ @Override
+ public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
+ final float velocityY) {
+ releaseCurrentKey();
+ return false;
+ }
+
+ @Override
+ public void onLongPress(final MotionEvent e) {
+ // Long press detection of {@link #mGestureDetector} is disabled and not used.
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java
new file mode 100644
index 000000000..d1ccdc7b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.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.keyboard.internal;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+/**
+ * This is an extended {@link ScrollView} that can notify
+ * {@link ScrollView#onScrollChanged(int,int,int,int} and
+ * {@link ScrollView#onOverScrolled(int,int,int,int)} to a content view.
+ */
+public class ScrollViewWithNotifier extends ScrollView {
+ private ScrollListener mScrollListener = EMPTY_LISTER;
+
+ public interface ScrollListener {
+ public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY);
+ public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX,
+ boolean clampedY);
+ }
+
+ private static final ScrollListener EMPTY_LISTER = new ScrollListener() {
+ @Override
+ public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY) {}
+ @Override
+ public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX,
+ boolean clampedY) {}
+ };
+
+ public ScrollViewWithNotifier(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onScrollChanged(final int scrollX, final int scrollY, final int oldX,
+ final int oldY) {
+ super.onScrollChanged(scrollX, scrollY, oldX, oldY);
+ mScrollListener.notifyScrollChanged(scrollX, scrollY, oldX, oldY);
+ }
+
+ @Override
+ protected void onOverScrolled(final int scrollX, final int scrollY, final boolean clampedX,
+ final boolean clampedY) {
+ super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+ mScrollListener.notifyOverScrolled(scrollX, scrollY, clampedX, clampedY);
+ }
+
+ public void setScrollListener(final ScrollListener listener) {
+ mScrollListener = listener;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
index ebbcedc96..55df263fe 100644
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -19,10 +19,11 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.util.Log;
+import com.android.inputmethod.latin.makedict.DictEncoder;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.makedict.Ver3DictEncoder;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
// TODO: Quit extending Dictionary after implementing dynamic binary dictionary.
@@ -42,37 +43,28 @@ abstract public class AbstractDictionaryWriter extends Dictionary {
abstract public void addUnigramWord(final String word, final String shortcutTarget,
final int frequency, final boolean isNotAWord);
+ // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve.
abstract public void addBigramWords(final String word0, final String word1,
- final int frequency, final boolean isValid);
+ final int frequency, final boolean isValid,
+ final long lastModifiedTime);
abstract public void removeBigramWords(final String word0, final String word1);
- abstract protected void writeBinaryDictionary(final FileOutputStream out)
+ abstract protected void writeDictionary(final DictEncoder dictEncoder)
throws IOException, UnsupportedFormatException;
public void write(final String fileName) {
final String tempFileName = fileName + ".temp";
final File file = new File(mContext.getFilesDir(), fileName);
final File tempFile = new File(mContext.getFilesDir(), tempFileName);
- FileOutputStream out = null;
try {
- out = new FileOutputStream(tempFile);
- writeBinaryDictionary(out);
- out.flush();
- out.close();
+ final DictEncoder dictEncoder = new Ver3DictEncoder(file);
+ writeDictionary(dictEncoder);
tempFile.renameTo(file);
} catch (IOException e) {
Log.e(TAG, "IO exception while writing file", e);
} catch (UnsupportedFormatException e) {
Log.e(TAG, "Unsupported format", e);
- } finally {
- if (out != null) {
- try {
- out.close();
- } catch (IOException e) {
- // ignore
- }
- }
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index 42c57946d..54bc29559 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -57,10 +57,10 @@ public final class AudioAndHapticFeedbackManager {
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
- public void hapticAndAudioFeedback(final int primaryCode,
+ public void performHapticAndAudioFeedback(final int code,
final View viewToPerformHapticFeedbackOn) {
- vibrateInternal(viewToPerformHapticFeedbackOn);
- playKeyClick(primaryCode);
+ performHapticFeedback(viewToPerformHapticFeedbackOn);
+ performAudioFeedback(code);
}
public boolean hasVibrator() {
@@ -81,14 +81,14 @@ public final class AudioAndHapticFeedbackManager {
return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
}
- private void playKeyClick(final int primaryCode) {
+ public void performAudioFeedback(final int code) {
// if mAudioManager is null, we can't play a sound anyway, so return
if (mAudioManager == null) {
return;
}
if (mSoundOn) {
final int sound;
- switch (primaryCode) {
+ switch (code) {
case Constants.CODE_DELETE:
sound = AudioManager.FX_KEYPRESS_DELETE;
break;
@@ -106,7 +106,7 @@ public final class AudioAndHapticFeedbackManager {
}
}
- private void vibrateInternal(final View viewToPerformHapticFeedbackOn) {
+ public void performHapticFeedback(final View viewToPerformHapticFeedbackOn) {
if (!mSettingsValues.mVibrateOn) {
return;
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d181bf697..e8b06570f 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -21,7 +21,6 @@ import android.util.SparseArray;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils;
import com.android.inputmethod.latin.settings.NativeSuggestOptions;
import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.JniUtils;
@@ -120,15 +119,16 @@ public final class BinaryDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
- 0 /* sessionId */);
+ additionalFeaturesOptions, 0 /* sessionId */);
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int sessionId) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId) {
if (!isValidDictionary()) return null;
Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
@@ -148,8 +148,7 @@ public final class BinaryDictionary extends Dictionary {
final InputPointers ips = composer.getInputPointers();
final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
mNativeSuggestOptions.setIsGesture(isGesture);
- mNativeSuggestOptions.setAdditionalFeaturesOptions(
- AdditionalFeaturesSettingUtils.getAdditionalNativeSuggestOptions());
+ mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
// proximityInfo and/or prevWordForBigrams may not be null.
final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
getTraverseSession(sessionId).getSession(), ips.getXCoordinates(),
@@ -179,7 +178,8 @@ public final class BinaryDictionary extends Dictionary {
// TODO: check that all users of the `kind' parameter are ready to accept
// flags too and pass mOutputTypes[j] instead of kind
suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
- score, kind, mDictType));
+ score, kind, this /* sourceDict */,
+ mSpaceIndices[0] /* indexOfTouchPointOfSecondWord */));
}
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index fa301b5a6..2b6d983c0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -21,17 +21,16 @@ import android.content.SharedPreferences;
import android.content.res.AssetFileDescriptor;
import android.util.Log;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
import com.android.inputmethod.latin.utils.LocaleUtils;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
-import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@@ -227,23 +226,12 @@ final public class BinaryDictionaryGetter {
// those do not include whitelist entries, the new code with an old version of the dictionary
// would lose whitelist functionality.
private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) {
- FileInputStream inStream = null;
try {
// Read the version of the file
- inStream = new FileInputStream(f);
- final BinaryDictInputOutput.ByteBufferWrapper buffer =
- new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, 0, f.length()));
- final int magic = buffer.readInt();
- if (magic != FormatSpec.VERSION_2_MAGIC_NUMBER) {
- return false;
- }
- final int formatVersion = buffer.readInt();
- final int headerSize = buffer.readInt();
- final HashMap<String, String> options = CollectionUtils.newHashMap();
- BinaryDictInputOutput.populateOptions(buffer, headerSize, options);
+ final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(f);
+ final FileHeader header = dictDecoder.readHeader();
- final String version = options.get(VERSION_KEY);
+ final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY);
if (null == version) {
// No version in the options : the format is unexpected
return false;
@@ -259,14 +247,8 @@ final public class BinaryDictionaryGetter {
return false;
} catch (BufferUnderflowException e) {
return false;
- } finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
+ } catch (UnsupportedFormatException e) {
+ return false;
}
}
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index c99d0e2ea..67eb7f3dd 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -70,7 +70,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
private final boolean mUseFirstLastBigrams;
public ContactsBinaryDictionary(final Context context, final Locale locale) {
- super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS);
+ super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS,
+ false /* isUpdatable */);
mLocale = locale;
mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
registerObserver(context);
@@ -208,7 +209,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
false /* isNotAWord */);
if (!TextUtils.isEmpty(prevWord)) {
if (mUseFirstLastBigrams) {
- super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
+ super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
+ 0 /* lastModifiedTime */);
}
}
prevWord = word;
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 7c3e4a740..8a3a88438 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -28,9 +28,26 @@ import java.util.ArrayList;
public abstract class Dictionary {
public static final int NOT_A_PROBABILITY = -1;
+ // The following types do not actually come from real dictionary instances, so we create
+ // corresponding instances.
public static final String TYPE_USER_TYPED = "user_typed";
+ public static final Dictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED);
+
public static final String TYPE_APPLICATION_DEFINED = "application_defined";
+ public static final Dictionary DICTIONARY_APPLICATION_DEFINED =
+ new PhonyDictionary(TYPE_APPLICATION_DEFINED);
+
public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
+ public static final Dictionary DICTIONARY_HARDCODED =
+ new PhonyDictionary(TYPE_HARDCODED);
+
+ // Spawned by resuming suggestions. Comes from a span that was in the TextView.
+ public static final String TYPE_RESUMED = "resumed";
+ public static final Dictionary DICTIONARY_RESUMED =
+ new PhonyDictionary(TYPE_RESUMED);
+
+ // The following types of dictionary have actual functional instances. We don't need final
+ // phony dictionary instances for them.
public static final String TYPE_MAIN = "main";
public static final String TYPE_CONTACTS = "contacts";
// User dictionary, the system-managed one.
@@ -42,9 +59,7 @@ public abstract class Dictionary {
// Personalization prediction dictionary internal to LatinIME's Java code.
public static final String TYPE_PERSONALIZATION_PREDICTION_IN_JAVA =
"personalization_prediction_in_java";
- // Spawned by resuming suggestions. Comes from a span that was in the TextView.
- public static final String TYPE_RESUMED = "resumed";
- protected final String mDictType;
+ public final String mDictType;
public Dictionary(final String dictType) {
mDictType = dictType;
@@ -57,20 +72,23 @@ public abstract class Dictionary {
* @param prevWord the previous word, or null if none
* @param proximityInfo the object for key proximity. May be ignored by some implementations.
* @param blockOffensiveWords whether to block potentially offensive words
+ * @param additionalFeaturesOptions options about additional features used for the suggestion.
* @return the list of suggestions (possibly null if none)
*/
// TODO: pass more context than just the previous word, to enable better suggestions (n-gram
// and more)
abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions);
// The default implementation of this method ignores sessionId.
// Subclasses that want to use sessionId need to override this method.
public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int sessionId) {
- return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId) {
+ return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
/**
@@ -114,8 +132,40 @@ public abstract class Dictionary {
/**
* Subclasses may override to indicate that this Dictionary is not yet properly initialized.
*/
-
public boolean isInitialized() {
return true;
}
+
+ /**
+ * Whether we think this suggestion should trigger an auto-commit.
+ */
+ public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
+ // If we don't have support for auto-commit, or if we don't know, we return false to
+ // avoid auto-committing stuff. Implementations of the Dictionary class that know to
+ // determine whether we should auto-commit will override this.
+ return false;
+ }
+
+ /**
+ * Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
+ * real dictionary.
+ */
+ private static class PhonyDictionary extends Dictionary {
+ // This class is not publicly instantiable.
+ private PhonyDictionary(final String type) {
+ super(type);
+ }
+
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ return null;
+ }
+
+ @Override
+ public boolean isValidWord(String word) {
+ return false;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index d05bb1e55..bf075140e 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -58,18 +58,18 @@ public final class DictionaryCollection extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
if (dictionaries.isEmpty()) return null;
// To avoid creating unnecessary objects, we get the list out of the first
// dictionary and add the rest to it if not null, hence the get(0)
ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
- prevWord, proximityInfo, blockOffensiveWords);
+ prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
if (null == suggestions) suggestions = CollectionUtils.newArrayList();
final int length = dictionaries.size();
for (int i = 1; i < length; ++ i) {
final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
- prevWord, proximityInfo, blockOffensiveWords);
+ prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
if (null != sugg) suggestions.addAll(sugg);
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index 47151bf61..5a453dde5 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -20,15 +20,14 @@ import android.content.Context;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.DictEncoder;
import com.android.inputmethod.latin.makedict.FormatSpec;
import com.android.inputmethod.latin.makedict.FusionDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import com.android.inputmethod.latin.utils.CollectionUtils;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -51,7 +50,7 @@ public class DictionaryWriter extends AbstractDictionaryWriter {
@Override
public void clear() {
final HashMap<String, String> attributes = CollectionUtils.newHashMap();
- mFusionDictionary = new FusionDictionary(new Node(),
+ mFusionDictionary = new FusionDictionary(new PtNodeArray(),
new FusionDictionary.DictionaryOptions(attributes, false, false));
}
@@ -75,7 +74,7 @@ public class DictionaryWriter extends AbstractDictionaryWriter {
@Override
public void addBigramWords(final String word0, final String word1, final int frequency,
- final boolean isValid) {
+ final boolean isValid, final long lastModifiedTime) {
mFusionDictionary.setBigram(word0, word1, frequency);
}
@@ -85,15 +84,15 @@ public class DictionaryWriter extends AbstractDictionaryWriter {
}
@Override
- protected void writeBinaryDictionary(final FileOutputStream out)
+ protected void writeDictionary(final DictEncoder dictEncoder)
throws IOException, UnsupportedFormatException {
- BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
+ dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS);
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- boolean blockOffensiveWords) {
+ boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
// This class doesn't support suggestion.
return null;
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 3f11391ba..b92283c5b 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -20,13 +20,16 @@ import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter;
import com.android.inputmethod.latin.utils.CollectionUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
@@ -78,12 +81,21 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
private final String mFilename;
+ /** Whether to support dynamically updating the dictionary */
+ private final boolean mIsUpdatable;
+
/** Controls access to the shared binary dictionary file across multiple instances. */
private final DictionaryController mSharedDictionaryController;
/** Controls access to the local binary dictionary for this instance. */
private final DictionaryController mLocalDictionaryController = new DictionaryController();
+ /* A extension for a binary dictionary file. */
+ public static final String DICT_FILE_EXTENSION = ".dict";
+
+ private final AtomicReference<AsyncWriteBinaryDictionaryTask> mWaitingTask =
+ new AtomicReference<AsyncWriteBinaryDictionaryTask>();
+
/**
* Abstract method for loading the unigrams and bigrams of a given dictionary in a background
* thread.
@@ -110,6 +122,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return controller;
}
+ private static AbstractDictionaryWriter getDictionaryWriter(final Context context,
+ final String dictType, final boolean isDynamicPersonalizationDictionary) {
+ if (isDynamicPersonalizationDictionary) {
+ return new DynamicPersonalizationDictionaryWriter(context, dictType);
+ } else {
+ return new DictionaryWriter(context, dictType);
+ }
+ }
+
/**
* Creates a new expandable binary dictionary.
*
@@ -117,19 +138,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* @param filename The filename for this binary dictionary. Multiple dictionaries with the same
* filename is supported.
* @param dictType the dictionary type, as a human-readable string
+ * @param isUpdatable whether to support dynamically updating the dictionary. Please note that
+ * dynamic dictionary has negative effects on memory space and computation time.
*/
- public ExpandableBinaryDictionary(
- final Context context, final String filename, final String dictType) {
+ public ExpandableBinaryDictionary(final Context context, final String filename,
+ final String dictType, final boolean isUpdatable) {
super(dictType);
mFilename = filename;
mContext = context;
+ mIsUpdatable = isUpdatable;
mBinaryDictionary = null;
mSharedDictionaryController = getSharedDictionaryController(filename);
- mDictionaryWriter = new DictionaryWriter(context, dictType);
+ // Currently, only dynamic personalization dictionary is updatable.
+ mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable);
}
protected static String getFilenameWithLocale(final String name, final String localeStr) {
- return name + "." + localeStr + ".dict";
+ return name + "." + localeStr + DICT_FILE_EXTENSION;
}
/**
@@ -137,6 +162,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
@Override
public void close() {
+ closeBinaryDictionary();
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ mDictionaryWriter.close();
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+
+ protected void closeBinaryDictionary() {
// Ensure that no other threads are accessing the local binary dictionary.
mLocalDictionaryController.writeLock().lock();
try {
@@ -144,7 +179,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
mBinaryDictionary.close();
mBinaryDictionary = null;
}
- mDictionaryWriter.close();
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+
+ protected void clear() {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ mDictionaryWriter.clear();
} finally {
mLocalDictionaryController.writeLock().unlock();
}
@@ -159,59 +202,95 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
/**
- * Sets a word bigram in the dictionary. Used for loading a dictionary.
+ * Adds a word bigram in the dictionary. Used for loading a dictionary.
*/
- protected void setBigram(final String prevWord, final String word, final int frequency) {
- mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
+ protected void addBigram(final String prevWord, final String word, final int frequency,
+ final long lastModifiedTime) {
+ mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */,
+ lastModifiedTime);
}
/**
- * Dynamically adds a word unigram to the dictionary.
+ * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry.
*/
protected void addWordDynamically(final String word, final String shortcutTarget,
final int frequency, final boolean isNotAWord) {
- mLocalDictionaryController.writeLock().lock();
- try {
- mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
- } finally {
- mLocalDictionaryController.writeLock().unlock();
+ if (!mIsUpdatable) {
+ Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
+ return;
+ }
+ // TODO: Use a queue to reflect what needs to be reflected.
+ if (mLocalDictionaryController.writeLock().tryLock()) {
+ try {
+ mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
}
}
/**
- * Dynamically sets a word bigram in the dictionary.
+ * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
*/
- protected void setBigramDynamically(final String prevWord, final String word,
- final int frequency) {
- mLocalDictionaryController.writeLock().lock();
- try {
- mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
- } finally {
- mLocalDictionaryController.writeLock().unlock();
+ protected void addBigramDynamically(final String word0, final String word1,
+ final int frequency, final boolean isValid) {
+ if (!mIsUpdatable) {
+ Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
+ + mFilename);
+ return;
+ }
+ // TODO: Use a queue to reflect what needs to be reflected.
+ if (mLocalDictionaryController.writeLock().tryLock()) {
+ try {
+ mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid,
+ 0 /* lastTouchedTime */);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+ }
+
+ /**
+ * Dynamically remove a word bigram in the dictionary.
+ */
+ protected void removeBigramDynamically(final String word0, final String word1) {
+ if (!mIsUpdatable) {
+ Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: "
+ + mFilename);
+ return;
+ }
+ // TODO: Use a queue to reflect what needs to be reflected.
+ if (mLocalDictionaryController.writeLock().tryLock()) {
+ try {
+ mDictionaryWriter.removeBigramWords(word0, word1);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
}
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
asyncReloadDictionaryIfRequired();
// Write lock because getSuggestions in native updates session status.
if (mLocalDictionaryController.writeLock().tryLock()) {
try {
final ArrayList<SuggestedWordInfo> inMemDictSuggestion =
mDictionaryWriter.getSuggestions(composer, prevWord, proximityInfo,
- blockOffensiveWords);
- if (mBinaryDictionary != null) {
+ blockOffensiveWords, additionalFeaturesOptions);
+ // TODO: Remove checking mIsUpdatable and use native suggestion.
+ if (mBinaryDictionary != null && !mIsUpdatable) {
final ArrayList<SuggestedWordInfo> binarySuggestion =
mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
- blockOffensiveWords);
+ blockOffensiveWords, additionalFeaturesOptions);
if (inMemDictSuggestion == null) {
return binarySuggestion;
} else if (binarySuggestion == null) {
return inMemDictSuggestion;
} else {
- binarySuggestion.addAll(binarySuggestion);
+ binarySuggestion.addAll(inMemDictSuggestion);
return binarySuggestion;
}
} else {
@@ -277,7 +356,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// Build the new binary dictionary
final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
- true /* useFullEditDistance */, null, mDictType, false /* isUpdatable */);
+ true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
if (mBinaryDictionary != null) {
// Ensure all threads accessing the current dictionary have finished before swapping in
@@ -302,9 +381,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
abstract protected boolean needsToReloadBeforeWriting();
/**
- * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
+ * Writes a new binary dictionary based on the contents of the fusion dictionary.
*/
- private void generateBinaryDictionary() {
+ private void writeBinaryDictionary() {
if (DEBUG) {
Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
+ mSharedDictionaryController.mLastUpdateRequestTime + " update="
@@ -337,7 +416,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/**
* Reloads the dictionary if required. Reload will occur asynchronously in a separate thread.
*/
- void asyncReloadDictionaryIfRequired() {
+ public void asyncReloadDictionaryIfRequired() {
if (!isReloadRequired()) return;
if (DEBUG) {
Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename);
@@ -348,7 +427,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/**
* Reloads the dictionary if required.
*/
- protected final void syncReloadDictionaryIfRequired() {
+ public final void syncReloadDictionaryIfRequired() {
if (!isReloadRequired()) return;
syncReloadDictionaryInternal();
}
@@ -367,41 +446,47 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
private final void syncReloadDictionaryInternal() {
// Ensure that only one thread attempts to read or write to the shared binary dictionary
// file at the same time.
- mSharedDictionaryController.writeLock().lock();
+ mLocalDictionaryController.writeLock().lock();
try {
- final long time = SystemClock.uptimeMillis();
- final boolean dictionaryFileExists = dictionaryFileExists();
- if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) {
- // If the shared dictionary file does not exist or is out of date, the first
- // instance that acquires the lock will generate a new one.
- if (hasContentChanged() || !dictionaryFileExists) {
- // If the source content has changed or the dictionary does not exist, rebuild
- // the binary dictionary. Empty dictionaries are supported (in the case where
- // loadDictionaryAsync() adds nothing) in order to provide a uniform framework.
+ mSharedDictionaryController.writeLock().lock();
+ try {
+ final long time = SystemClock.uptimeMillis();
+ final boolean dictionaryFileExists = dictionaryFileExists();
+ if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) {
+ // If the shared dictionary file does not exist or is out of date, the first
+ // instance that acquires the lock will generate a new one.
+ if (hasContentChanged() || !dictionaryFileExists) {
+ // If the source content has changed or the dictionary does not exist,
+ // rebuild the binary dictionary. Empty dictionaries are supported (in the
+ // case where loadDictionaryAsync() adds nothing) in order to provide a
+ // uniform framework.
+ mSharedDictionaryController.mLastUpdateTime = time;
+ writeBinaryDictionary();
+ loadBinaryDictionary();
+ } else {
+ // If not, the reload request was unnecessary so revert
+ // LastUpdateRequestTime to LastUpdateTime.
+ mSharedDictionaryController.mLastUpdateRequestTime =
+ mSharedDictionaryController.mLastUpdateTime;
+ }
+ } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
+ < mSharedDictionaryController.mLastUpdateTime) {
+ // Otherwise, if the local dictionary is older than the shared dictionary, load
+ // the shared dictionary.
+ loadBinaryDictionary();
+ }
+ if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
+ // Binary dictionary is not valid. Regenerate the dictionary file.
mSharedDictionaryController.mLastUpdateTime = time;
- generateBinaryDictionary();
+ writeBinaryDictionary();
loadBinaryDictionary();
- } else {
- // If not, the reload request was unnecessary so revert LastUpdateRequestTime
- // to LastUpdateTime.
- mSharedDictionaryController.mLastUpdateRequestTime =
- mSharedDictionaryController.mLastUpdateTime;
}
- } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
- < mSharedDictionaryController.mLastUpdateTime) {
- // Otherwise, if the local dictionary is older than the shared dictionary, load the
- // shared dictionary.
- loadBinaryDictionary();
- }
- if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
- // Binary dictionary is not valid. Regenerate the dictionary file.
- mSharedDictionaryController.mLastUpdateTime = time;
- generateBinaryDictionary();
- loadBinaryDictionary();
+ mLocalDictionaryController.mLastUpdateTime = time;
+ } finally {
+ mSharedDictionaryController.writeLock().unlock();
}
- mLocalDictionaryController.mLastUpdateTime = time;
} finally {
- mSharedDictionaryController.writeLock().unlock();
+ mLocalDictionaryController.writeLock().unlock();
}
}
@@ -422,6 +507,68 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
/**
+ * Load the dictionary to memory.
+ */
+ protected void asyncLoadDictionaryToMemory() {
+ new AsyncLoadDictionaryToMemoryTask().start();
+ }
+
+ /**
+ * Thread class for asynchronously loading dictionary to memory.
+ */
+ private class AsyncLoadDictionaryToMemoryTask extends Thread {
+ @Override
+ public void run() {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ mSharedDictionaryController.readLock().lock();
+ try {
+ loadDictionaryAsync();
+ } finally {
+ mSharedDictionaryController.readLock().unlock();
+ }
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+ }
+
+ /**
+ * Generate binary dictionary using DictionaryWriter.
+ */
+ protected void asyncWriteBinaryDictionary() {
+ final AsyncWriteBinaryDictionaryTask newTask = new AsyncWriteBinaryDictionaryTask();
+ newTask.start();
+ final AsyncWriteBinaryDictionaryTask oldTask = mWaitingTask.getAndSet(newTask);
+ if (oldTask != null) {
+ oldTask.interrupt();
+ }
+ }
+
+ /**
+ * Thread class for asynchronously writing the binary dictionary.
+ */
+ private class AsyncWriteBinaryDictionaryTask extends Thread {
+ @Override
+ public void run() {
+ mSharedDictionaryController.writeLock().lock();
+ try {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ if (isInterrupted()) {
+ return;
+ }
+ writeBinaryDictionary();
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ } finally {
+ mSharedDictionaryController.writeLock().unlock();
+ }
+ }
+ }
+
+ /**
* Lock for controlling access to a given binary dictionary and for tracking whether the
* dictionary is out of date. Can be shared across multiple dictionary instances that access the
* same filename.
@@ -434,4 +581,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return (mLastUpdateRequestTime > mLastUpdateTime);
}
}
+
+ /**
+ * Dynamically adds a word unigram to the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void addWordDynamicallyForTests(final String word, final String shortcutTarget,
+ final int frequency, final boolean isNotAWord) {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ addWordDynamically(word, shortcutTarget, frequency, isNotAWord);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Dynamically adds a word bigram in the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void addBigramDynamicallyForTests(final String word0, final String word1,
+ final int frequency, final boolean isValid) {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ addBigramDynamically(word0, word1, frequency, isValid);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Dynamically remove a word bigram in the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void removeBigramDynamicallyForTests(final String word0, final String word1) {
+ mLocalDictionaryController.writeLock().lock();
+ try {
+ removeBigramDynamically(word0, word1);
+ } finally {
+ mLocalDictionaryController.writeLock().unlock();
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index bd2d70365..342dcfc5e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -16,10 +16,10 @@
package com.android.inputmethod.latin;
-import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -29,9 +29,10 @@ import java.util.ArrayList;
import java.util.LinkedList;
/**
- * Base class for an in-memory dictionary that can grow dynamically and can
+ * Class for an in-memory dictionary that can grow dynamically and can
* be searched for suggestions and valid words.
*/
+// TODO: Remove after binary dictionary supports dynamic update.
public class ExpandableDictionary extends Dictionary {
private static final String TAG = ExpandableDictionary.class.getSimpleName();
/**
@@ -39,23 +40,11 @@ public class ExpandableDictionary extends Dictionary {
*/
private static final int FULL_WORD_SCORE_MULTIPLIER = 2;
- // Bigram frequency is a fixed point number with 1 meaning 1.2 and 255 meaning 1.8.
- protected static final int BIGRAM_MAX_FREQUENCY = 255;
-
- private Context mContext;
private char[] mWordBuilder = new char[Constants.DICTIONARY_MAX_WORD_LENGTH];
private int mMaxDepth;
private int mInputLength;
- private boolean mRequiresReload;
-
- private boolean mUpdatingDictionary;
-
- // Use this lock before touching mUpdatingDictionary & mRequiresDownload
- private Object mUpdatingLock = new Object();
-
private static final class Node {
- Node() {}
char mCode;
int mFrequency;
boolean mTerminal;
@@ -157,46 +146,12 @@ public class ExpandableDictionary extends Dictionary {
private int[][] mCodes;
- public ExpandableDictionary(final Context context, final String dictType) {
+ public ExpandableDictionary(final String dictType) {
super(dictType);
- mContext = context;
clearDictionary();
mCodes = new int[Constants.DICTIONARY_MAX_WORD_LENGTH][];
}
- public void loadDictionary() {
- synchronized (mUpdatingLock) {
- startDictionaryLoadingTaskLocked();
- }
- }
-
- public void startDictionaryLoadingTaskLocked() {
- if (!mUpdatingDictionary) {
- mUpdatingDictionary = true;
- mRequiresReload = false;
- new LoadDictionaryTask().start();
- }
- }
-
- public void setRequiresReload(final boolean reload) {
- synchronized (mUpdatingLock) {
- mRequiresReload = reload;
- }
- }
-
- public boolean getRequiresReload() {
- return mRequiresReload;
- }
-
- /** Override to load your dictionary here, on a background thread. */
- public void loadDictionaryAsync() {
- // empty base implementation
- }
-
- public Context getContext() {
- return mContext;
- }
-
public int getMaxWordLength() {
return Constants.DICTIONARY_MAX_WORD_LENGTH;
}
@@ -231,7 +186,7 @@ public class ExpandableDictionary extends Dictionary {
childNode.mShortcutOnly = isShortcutOnly;
children.add(childNode);
}
- if (wordLength == depth + 1 && shortcutTarget != null) {
+ if (wordLength == depth + 1) {
// Terminate this word
childNode.mTerminal = true;
if (isShortcutOnly) {
@@ -255,8 +210,7 @@ public class ExpandableDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
- if (reloadDictionaryIfRequired()) return null;
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
if (composer.size() > 1) {
if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
return null;
@@ -272,17 +226,7 @@ public class ExpandableDictionary extends Dictionary {
}
}
- // This reloads the dictionary if required, and returns whether it's currently updating its
- // contents or not.
- private boolean reloadDictionaryIfRequired() {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- return mUpdatingDictionary;
- }
- }
-
- protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+ private ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo) {
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
mInputLength = codes.size();
@@ -312,11 +256,6 @@ public class ExpandableDictionary extends Dictionary {
@Override
public synchronized boolean isValidWord(final String word) {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- if (mUpdatingDictionary) return false;
- }
final Node node = searchNode(mRoots, word, 0, word.length());
// If node is null, we didn't find the word, so it's not valid.
// If node.mShortcutOnly is true, then it exists as a shortcut but not as a word,
@@ -326,7 +265,7 @@ public class ExpandableDictionary extends Dictionary {
return (node == null) ? false : !node.mShortcutOnly;
}
- protected boolean removeBigram(final String word1, final String word2) {
+ public boolean removeBigram(final String word1, final String word2) {
// Refer to addOrSetBigram() about word1.toLowerCase()
final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
final Node secondWord = searchWord(mRoots, word2, 0, null);
@@ -351,13 +290,14 @@ public class ExpandableDictionary extends Dictionary {
/**
* Returns the word's frequency or -1 if not found
*/
- protected int getWordFrequency(final String word) {
+ @UsedForTesting
+ public int getWordFrequency(final String word) {
// Case-sensitive search
final Node node = searchNode(mRoots, word, 0, word.length());
return (node == null) ? -1 : node.mFrequency;
}
- protected NextWord getBigramWord(final String word1, final String word2) {
+ public NextWord getBigramWord(final String word1, final String word2) {
// Refer to addOrSetBigram() about word1.toLowerCase()
final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
final Node secondWord = searchWord(mRoots, word2, 0, null);
@@ -403,7 +343,8 @@ public class ExpandableDictionary extends Dictionary {
// the respective size of the typed word and the suggestion if it matters sometime
// in the future.
suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
- SuggestedWordInfo.KIND_CORRECTION, mDictType));
+ SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
}
if (null != node.mShortcutTargets) {
@@ -411,7 +352,8 @@ public class ExpandableDictionary extends Dictionary {
for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
- finalFreq, SuggestedWordInfo.KIND_SHORTCUT, mDictType));
+ finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
}
}
@@ -438,7 +380,7 @@ public class ExpandableDictionary extends Dictionary {
* @param suggestions the list in which to add suggestions
*/
// TODO: Share this routine with the native code for BinaryDictionary
- protected void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word,
+ private void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word,
final int depth, final boolean completion, final int snr, final int inputIndex,
final int skipPos, final ArrayList<SuggestedWordInfo> suggestions) {
final int count = roots.mLength;
@@ -657,7 +599,8 @@ public class ExpandableDictionary extends Dictionary {
if (freq >= 0 && node == null) {
suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
Constants.DICTIONARY_MAX_WORD_LENGTH - index),
- freq, SuggestedWordInfo.KIND_CORRECTION, mDictType));
+ freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
}
}
}
@@ -695,21 +638,10 @@ public class ExpandableDictionary extends Dictionary {
return null;
}
- protected void clearDictionary() {
+ public void clearDictionary() {
mRoots = new NodeArray();
}
- private final class LoadDictionaryTask extends Thread {
- LoadDictionaryTask() {}
- @Override
- public void run() {
- loadDictionaryAsync();
- synchronized (mUpdatingLock) {
- mUpdatingDictionary = false;
- }
- }
- }
-
private static char toLowerCase(final char c) {
char baseChar = c;
if (c < BASE_CHARS.length) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 5c5b7b7c0..6be9ded5c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -73,9 +73,12 @@ import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
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.PersonalizationDictionaryHelper;
+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.settings.Settings;
@@ -96,6 +99,7 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus;
import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
import com.android.inputmethod.latin.utils.TextRange;
+import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
import com.android.inputmethod.research.ResearchLogger;
import java.io.FileDescriptor;
@@ -103,6 +107,8 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Locale;
import java.util.TreeSet;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* Input method implementation for Qwerty'ish keyboard.
@@ -123,6 +129,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private static final int PENDING_IMS_CALLBACK_DURATION = 800;
+ private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
+
+ // TODO: Set this value appropriately.
+ private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
+
/**
* The name of the scheme used by the Package Manager to warn of a new package installation,
* replacement or removal.
@@ -171,6 +182,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private UserBinaryDictionary mUserDictionary;
private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary;
private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary;
+ private PersonalizationDictionary mPersonalizationDictionary;
private boolean mIsUserDictionaryAvailable;
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
@@ -190,7 +202,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mExpectingUpdateSelection;
private int mDeleteCount;
private long mLastKeyTime;
- private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
+ private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
+ // Personalization debugging params
+ private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
+ private boolean mBoostPersonalizationDictionaryForDebug = false;
// Member variables for remembering the current device orientation.
private int mDisplayOrientation;
@@ -211,6 +226,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private final boolean mIsHardwareAcceleratedDrawingEnabled;
public final UIHandler mHandler = new UIHandler(this);
+ private InputUpdater mInputUpdater;
public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
@@ -219,8 +235,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
private static final int MSG_RESUME_SUGGESTIONS = 4;
private static final int MSG_REOPEN_DICTIONARIES = 5;
+ private static final int MSG_ON_END_BATCH_INPUT = 6;
+ private static final int ARG1_NOT_GESTURE_INPUT = 0;
private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
+ private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
private int mDelayUpdateSuggestions;
private int mDelayUpdateShiftState;
@@ -253,8 +272,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
switcher.updateShiftState();
break;
case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
- latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
- msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+ if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
+ latinIme.showSuggestionStrip((SuggestedWords) msg.obj);
+ } else {
+ latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
+ msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+ }
break;
case MSG_RESUME_SUGGESTIONS:
latinIme.restartSuggestionsOnWordTouchedByCursor();
@@ -266,6 +289,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// get any suggestions. Wait one frame.
postUpdateSuggestionStrip();
break;
+ case MSG_ON_END_BATCH_INPUT:
+ latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
+ break;
}
}
@@ -307,11 +333,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final boolean dismissGestureFloatingPreviewText) {
removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
final int arg1 = dismissGestureFloatingPreviewText
- ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0;
+ ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
+ : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords)
.sendToTarget();
}
+ public void showSuggestionStrip(final SuggestedWords suggestedWords) {
+ removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+ obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
+ ARG1_NOT_GESTURE_INPUT, 0, suggestedWords).sendToTarget();
+ }
+
+ public void onEndBatchInput(final SuggestedWords suggestedWords) {
+ obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget();
+ }
+
public void startDoubleSpacePeriodTimer() {
mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis();
}
@@ -472,6 +509,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
KeyboardSwitcher.init(this);
AudioAndHapticFeedbackManager.init(this);
AccessibilityUtils.init(this);
+ PersonalizationDictionarySessionRegister.init(this);
super.onCreate();
@@ -503,6 +541,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final IntentFilter newDictFilter = new IntentFilter();
newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
+
+ mInputUpdater = new InputUpdater(this);
}
// Has to be package-visible for unit tests
@@ -558,10 +598,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- mUserHistoryPredictionDictionary = PersonalizationDictionaryHelper
+ mUserHistoryPredictionDictionary = PersonalizationHelper
.getUserHistoryPredictionDictionary(this, localeStr, prefs);
newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary);
- mPersonalizationPredictionDictionary = PersonalizationDictionaryHelper
+ mPersonalizationDictionary = PersonalizationHelper
+ .getPersonalizationDictionary(this, localeStr, prefs);
+ newSuggest.setPersonalizationDictionary(mPersonalizationDictionary);
+ mPersonalizationPredictionDictionary = PersonalizationHelper
.getPersonalizationPredictionDictionary(this, localeStr, prefs);
newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary);
@@ -633,8 +676,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
ResearchLogger.getInstance().onDestroy();
}
unregisterReceiver(mDictionaryPackInstallReceiver);
+ PersonalizationDictionarySessionRegister.onDestroy(this);
LatinImeLogger.commit();
LatinImeLogger.onDestroy();
+ if (mInputUpdater != null) {
+ mInputUpdater.onDestroy();
+ mInputUpdater = null;
+ }
super.onDestroy();
}
@@ -652,6 +700,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mOptionsDialog.dismiss();
}
}
+ PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
super.onConfigurationChanged(conf);
}
@@ -863,9 +912,35 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// be replaced when the user dictionary reports back with the actual word, which ends
// up calling #onWordAddedToUserDictionary() in this class.
+ initPersonalizationDebugSettings(currentSettingsValues);
+
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
+ // Initialization of personalization debug settings. This must be called inside
+ // onStartInputView.
+ private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
+ if (mUseOnlyPersonalizationDictionaryForDebug
+ != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
+ // Only for debug
+ initSuggest();
+ mUseOnlyPersonalizationDictionaryForDebug =
+ currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
+ }
+
+ if (mBoostPersonalizationDictionaryForDebug !=
+ currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
+ // Only for debug
+ mBoostPersonalizationDictionaryForDebug =
+ currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
+ if (mBoostPersonalizationDictionaryForDebug) {
+ UserHistoryForgettingCurveUtils.boostMaxFreqForDebug();
+ } else {
+ UserHistoryForgettingCurveUtils.resetMaxFreqForDebug();
+ }
+ }
+ }
+
// Callback for the TargetPackageInfoGetterTask
@Override
public void onTargetPackageInfoKnown(final PackageInfo info) {
@@ -1135,11 +1210,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return currentHeight;
}
- final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (mainKeyboardView == null) {
+ final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
+ if (visibleKeyboardView == null) {
return 0;
}
- final int keyboardHeight = mainKeyboardView.getHeight();
+ // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!!
+ // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ final int keyboardHeight = visibleKeyboardView.getHeight();
final int suggestionsHeight = mSuggestionStripView.getHeight();
final int displayHeight = getResources().getDisplayMetrics().heightPixels;
final Rect rect = new Rect();
@@ -1157,8 +1234,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onComputeInsets(final InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
- final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (mainKeyboardView == null || mSuggestionStripView == null) {
+ final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
+ if (visibleKeyboardView == null || mSuggestionStripView == null) {
return;
}
final int adjustedBackingHeight = getAdjustedBackingViewHeight();
@@ -1173,13 +1250,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
int visibleTopY = extraHeight;
// Need to set touchable region only if input view is being shown
- if (mainKeyboardView.isShown()) {
+ if (visibleKeyboardView.isShown()) {
if (mSuggestionStripView.getVisibility() == View.VISIBLE) {
visibleTopY -= suggestionsHeight;
}
- final int touchY = mainKeyboardView.isShowingMoreKeysPanel() ? 0 : visibleTopY;
- final int touchWidth = mainKeyboardView.getWidth();
- final int touchHeight = mainKeyboardView.getHeight() + extraHeight
+ final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
+ final int touchWidth = visibleKeyboardView.getWidth();
+ final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
// Extend touchable region below the keyboard.
+ EXTENDED_TOUCHABLE_REGION_HEIGHT;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
@@ -1368,10 +1445,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- private static boolean isAlphabet(final int code) {
- return Character.isLetter(code);
- }
-
private void onSettingsKeyPressed() {
if (isShowingOptionDialog()) return;
showSubtypeSelectorAndSettings();
@@ -1479,7 +1552,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
case Constants.CODE_SHIFT:
// Note: Calling back to the keyboard on Shift key is handled in
- // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
final Keyboard currentKeyboard = switcher.getKeyboard();
if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
// TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
@@ -1493,7 +1566,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
// Note: Calling back to the keyboard on symbol key is handled in
- // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SETTINGS:
onSettingsKeyPressed();
@@ -1511,7 +1584,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
handleLanguageSwitchKey();
break;
case Constants.CODE_EMOJI:
- // TODO: Implement emoji keyboard switch.
+ // Note: Switching emoji keyboard is being handled in
+ // {@link KeyboardState#onCodeInput(int,int)}.
break;
case Constants.CODE_ENTER:
final EditorInfo editorInfo = getCurrentInputEditorInfo();
@@ -1627,7 +1701,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onStartBatchInput() {
- BatchInputUpdater.getInstance().onStartBatchInput(this);
+ mInputUpdater.onStartBatchInput();
mHandler.cancelUpdateSuggestionStrip();
mConnection.beginBatchEdit();
final SettingsValues settingsValues = mSettings.getCurrent();
@@ -1667,46 +1741,41 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
}
- private static final class BatchInputUpdater implements Handler.Callback {
+ private static final class InputUpdater implements Handler.Callback {
private final Handler mHandler;
- private LatinIME mLatinIme;
+ private final LatinIME mLatinIme;
private final Object mLock = new Object();
private boolean mInBatchInput; // synchronized using {@link #mLock}.
- private BatchInputUpdater() {
+ private InputUpdater(final LatinIME latinIme) {
final HandlerThread handlerThread = new HandlerThread(
- BatchInputUpdater.class.getSimpleName());
+ InputUpdater.class.getSimpleName());
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), this);
- }
-
- // Initialization-on-demand holder
- private static final class OnDemandInitializationHolder {
- public static final BatchInputUpdater sInstance = new BatchInputUpdater();
- }
-
- public static BatchInputUpdater getInstance() {
- return OnDemandInitializationHolder.sInstance;
+ mLatinIme = latinIme;
}
private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
+ private static final int MSG_GET_SUGGESTED_WORDS = 2;
@Override
public boolean handleMessage(final Message msg) {
switch (msg.what) {
- case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
- updateBatchInput((InputPointers)msg.obj);
- break;
+ case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+ updateBatchInput((InputPointers)msg.obj);
+ break;
+ case MSG_GET_SUGGESTED_WORDS:
+ mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj);
+ break;
}
return true;
}
// Run in the UI thread.
- public void onStartBatchInput(final LatinIME latinIme) {
+ public void onStartBatchInput() {
synchronized (mLock) {
mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
mInBatchInput = true;
- mLatinIme = latinIme;
mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
}
@@ -1719,9 +1788,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Batch input has ended or canceled while the message was being delivered.
return;
}
- final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- suggestedWords, false /* dismissGestureFloatingPreviewText */);
+
+ getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+ suggestedWords, false /* dismissGestureFloatingPreviewText */);
+ }
+ });
}
}
@@ -1744,35 +1818,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Run in the UI thread.
- public SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
- synchronized (mLock) {
- mInBatchInput = false;
- final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- suggestedWords, true /* dismissGestureFloatingPreviewText */);
- return suggestedWords;
+ public void onEndBatchInput(final InputPointers batchPointers) {
+ synchronized(mLock) {
+ getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ mInBatchInput = false;
+ mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
+ true /* dismissGestureFloatingPreviewText */);
+ mLatinIme.mHandler.onEndBatchInput(suggestedWords);
+ }
+ });
}
}
// {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
// be synchronized.
- private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) {
+ private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
+ final OnGetSuggestedWordsCallback callback) {
mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
- final SuggestedWords suggestedWords =
- mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE);
- final int suggestionCount = suggestedWords.size();
- if (suggestionCount <= 1) {
- final String mostProbableSuggestion = (suggestionCount == 0) ? null
- : suggestedWords.getWord(0);
- return mLatinIme.getOlderSuggestions(mostProbableSuggestion);
- }
- return suggestedWords;
+ mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
+ new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(SuggestedWords suggestedWords) {
+ final int suggestionCount = suggestedWords.size();
+ if (suggestionCount <= 1) {
+ final String mostProbableSuggestion = (suggestionCount == 0) ? null
+ : suggestedWords.getWord(0);
+ callback.onGetSuggestedWords(
+ mLatinIme.getOlderSuggestions(mostProbableSuggestion));
+ }
+ callback.onGetSuggestedWords(suggestedWords);
+ }
+ });
+ }
+
+ public void getSuggestedWords(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
+ mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget();
+ }
+
+ private void onDestroy() {
+ mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
+ mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+ mHandler.getLooper().quit();
}
}
+ // This method must run in UI Thread.
private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
final boolean dismissGestureFloatingPreviewText) {
- showSuggestionStrip(suggestedWords, null);
+ showSuggestionStrip(suggestedWords);
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
if (dismissGestureFloatingPreviewText) {
@@ -1782,13 +1878,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onUpdateBatchInput(final InputPointers batchPointers) {
- BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers);
+ final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
+ if (null != candidate) {
+ if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
+ // TODO: implement auto-commit
+ }
+ }
+ mInputUpdater.onUpdateBatchInput(batchPointers);
}
- @Override
- public void onEndBatchInput(final InputPointers batchPointers) {
- final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput(
- batchPointers);
+ // This method must run in UI Thread.
+ public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
final String batchInputText = suggestedWords.isEmpty()
? null : suggestedWords.getWord(0);
if (TextUtils.isEmpty(batchInputText)) {
@@ -1810,6 +1910,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mKeyboardSwitcher.updateShiftState();
}
+ @Override
+ public void onEndBatchInput(final InputPointers batchPointers) {
+ mInputUpdater.onEndBatchInput(batchPointers);
+ }
+
private String specificTldProcessingOnTextInput(final String text) {
if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
|| !Character.isLetter(text.charAt(1))) {
@@ -1845,7 +1950,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onCancelBatchInput() {
- BatchInputUpdater.getInstance().onCancelBatchInput();
+ mInputUpdater.onCancelBatchInput();
}
private void handleBackspace(final int spaceState) {
@@ -1861,23 +1966,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// When we exit this if-clause, mWordComposer.isComposingWord() will return false.
}
if (mWordComposer.isComposingWord()) {
- final int length = mWordComposer.size();
- if (length > 0) {
- if (mWordComposer.isBatchMode()) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- final String word = mWordComposer.getTypedWord();
- ResearchLogger.latinIME_handleBackspace_batch(word, 1);
- }
- final String rejectedSuggestion = mWordComposer.getTypedWord();
- mWordComposer.reset();
- mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
- } else {
- mWordComposer.deleteLast();
+ if (mWordComposer.isBatchMode()) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ final String word = mWordComposer.getTypedWord();
+ ResearchLogger.latinIME_handleBackspace_batch(word, 1);
}
- mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- mHandler.postUpdateSuggestionStrip();
+ final String rejectedSuggestion = mWordComposer.getTypedWord();
+ mWordComposer.reset();
+ mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
} else {
- mConnection.deleteSurroundingText(1, 0);
+ mWordComposer.deleteLast();
+ }
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ mHandler.postUpdateSuggestionStrip();
+ if (!mWordComposer.isComposingWord()) {
+ // If we just removed the last character, auto-caps mode may have changed so we
+ // need to re-evaluate.
+ mKeyboardSwitcher.updateShiftState();
}
} else {
final SettingsValues currentSettings = mSettings.getCurrent();
@@ -1892,8 +1997,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Cancel multi-character input: remove the text we just entered.
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
- final int length = mEnteredText.length();
- mConnection.deleteSurroundingText(length, 0);
+ mConnection.deleteSurroundingText(mEnteredText.length(), 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
}
@@ -1939,6 +2043,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This should never happen.
Log.e(TAG, "Backspace when we don't know the selection position");
}
+ 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
@@ -1946,22 +2052,28 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// relying on this behavior so we continue to support it for older apps.
sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
} else {
- mConnection.deleteSurroundingText(1, 0);
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1, true /* shouldUncommitLogUnit */);
+ ResearchLogger.latinIME_handleBackspace(lengthToDelete,
+ true /* shouldUncommitLogUnit */);
}
if (mDeleteCount > DELETE_ACCELERATE_AT) {
- mConnection.deleteSurroundingText(1, 0);
+ final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
+ mConnection.getCodePointBeforeCursor()) ? 2 : 1;
+ mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1,
+ ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
true /* shouldUncommitLogUnit */);
}
}
}
- if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
+ && currentSettings.mCurrentLanguageHasSpaces) {
restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
}
+ // We just removed a character. We need to update the auto-caps state.
+ mKeyboardSwitcher.updateShiftState();
}
}
@@ -1986,6 +2098,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void handleCharacter(final int primaryCode, final int x,
final int y, final int spaceState) {
+ // TODO: refactor this method to stop flipping isComposingWord around all the time, and
+ // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
+ // which has the same name as other handle* methods but is not the same.
boolean isComposingWord = mWordComposer.isComposingWord();
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
@@ -2005,18 +2120,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
resetEntireInputState(mLastSelectionStart);
isComposingWord = false;
}
- // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
- // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
- // thread here.
- if (!isComposingWord && (isAlphabet(primaryCode)
- || currentSettings.isWordConnector(primaryCode))
+ // We want to find out whether to start composing a new word with this character. If so,
+ // we need to reset the composing state and switch isComposingWord. The order of the
+ // tests is important for good performance.
+ // We only start composing if we're not already composing.
+ if (!isComposingWord
+ // We only start composing if this is a word code point. Essentially that means it's a
+ // a letter or a word connector.
+ && currentSettings.isWordCodePoint(primaryCode)
+ // We never go into composing state if suggestions are not requested.
&& currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
- !mConnection.isCursorTouchingWord(currentSettings)) {
+ // In languages with spaces, we only start composing a word when we are not already
+ // touching a word. In languages without spaces, the above conditions are sufficient.
+ (!mConnection.isCursorTouchingWord(currentSettings)
+ || !currentSettings.mCurrentLanguageHasSpaces)) {
// Reset entirely the composing state anyway, then start composing a new word unless
- // the character is a single quote. The idea here is, single quote is not a
- // separator and it should be treated as a normal character, except in the first
- // position where it should not start composing a word.
- isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode);
+ // the character is a single quote or a dash. The idea here is, single quote and dash
+ // are not separators and they should be treated as normal characters, except in the
+ // first position where they should not start composing a word.
+ isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
+ && Constants.CODE_DASH != primaryCode);
// Here we don't need to reset the last composed word. It will be reset
// when we commit this one, if we ever do; if on the other hand we backspace
// it entirely and resume suggestions on the previous word, we'd like to still
@@ -2099,16 +2222,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean handleSeparator(final int primaryCode, final int x, final int y,
final int spaceState) {
boolean didAutoCorrect = false;
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ // We avoid sending spaces in languages without spaces if we were composing.
+ final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
+ && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can insert the separator at the current cursor position.
resetEntireInputState(mLastSelectionStart);
}
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (mWordComposer.isComposingWord()) {
+ if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
if (currentSettings.mCorrectionEnabled) {
- // TODO: maybe cache Strings in an <String> sparse array or something
- commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
+ final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
+ : new String(new int[] { primaryCode }, 0, 1);
+ commitCurrentAutoCorrection(separator);
didAutoCorrect = true;
} else {
commitTyped(new String(new int[]{primaryCode}, 0, 1));
@@ -2125,7 +2252,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
}
- sendKeyCodePoint(primaryCode);
+
+ if (!shouldAvoidSendingCode) {
+ sendKeyCodePoint(primaryCode);
+ }
if (Constants.CODE_SPACE == primaryCode) {
if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
@@ -2255,34 +2385,71 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return;
}
- final SuggestedWords suggestedWords =
- getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING);
- final String typedWord = mWordComposer.getTypedWord();
- showSuggestionStrip(suggestedWords, typedWord);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SuggestedWords[] suggestedWordsArray = new SuggestedWords[1];
+ getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
+ new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ suggestedWordsArray[0] = suggestedWords;
+ latch.countDown();
+ }
+ }
+ );
+
+ // TODO: Quit blocking the main thread.
+ try {
+ // Wait for the result of getSuggestedWords
+ // We set the time out to avoid ANR.
+ latch.await(GET_SUGGESTED_WORDS_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // TODO: Cancel all pending "getSuggestedWords" tasks when it failed. We may want to add
+ // "onGetSuggestionFailed" to "OnGetSuggestedWordsCallback".
+ Log.e(TAG, "InterruptedException while waiting for getSuggestedWords.", e);
+ return;
+ }
+ if (suggestedWordsArray[0] != null) {
+ showSuggestionStrip(suggestedWordsArray[0]);
+ }
}
- private SuggestedWords getSuggestedWords(final int sessionId) {
+ private void getSuggestedWords(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
final Suggest suggest = mSuggest;
if (keyboard == null || suggest == null) {
- return SuggestedWords.EMPTY;
+ callback.onGetSuggestedWords(SuggestedWords.EMPTY);
+ return;
}
// Get the word on which we should search the bigrams. If we are composing a word, it's
// whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
// should just skip whitespace if any, so 1.
- // TODO: this is slow (2-way IPC) - we should probably cache this instead.
final SettingsValues currentSettings = mSettings.getCurrent();
- final String prevWord =
- mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
- mWordComposer.isComposingWord() ? 2 : 1);
- return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
- currentSettings.mBlockPotentiallyOffensive,
- currentSettings.mCorrectionEnabled, sessionId);
+ final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
+ final String prevWord;
+ if (currentSettings.mCurrentLanguageHasSpaces) {
+ // If we are typing in a language with spaces we can just look up the previous
+ // word from textview.
+ prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
+ mWordComposer.isComposingWord() ? 2 : 1);
+ } else {
+ prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
+ : mLastComposedWord.mCommittedWord;
+ }
+ suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
+ currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
+ additionalFeaturesOptions, sessionId, callback);
}
- private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) {
- return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(),
- getSuggestedWords(sessionId));
+ private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
+ mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(SuggestedWords suggestedWords) {
+ callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
+ mWordComposer.getTypedWord(), suggestedWords));
+ }
+ });
}
private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
@@ -2322,16 +2489,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
false /* isPrediction */);
}
- private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) {
+ private void showSuggestionStrip(final SuggestedWords suggestedWords) {
if (suggestedWords.isEmpty()) {
clearSuggestionStrip();
return;
}
final String autoCorrection;
if (suggestedWords.mWillAutoCorrect) {
- autoCorrection = suggestedWords.getWord(1);
+ autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
} else {
- autoCorrection = typedWord;
+ autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD);
}
mWordComposer.setAutoCorrection(autoCorrection);
final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
@@ -2440,7 +2607,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
- suggestionInfo.mSourceDict);
+ suggestionInfo.mSourceDict.mDictType);
}
mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
@@ -2544,6 +2711,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// recorrection. This is a temporary, stopgap measure that will be removed later.
// TODO: remove this.
if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
+ // Recorrection is not supported in languages without spaces because we don't know
+ // how to segment them yet.
+ if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
// If the cursor is not touching a word, or if there is a selection, return right away.
if (mLastSelectionStart != mLastSelectionEnd) return;
// If we don't know the cursor location, return.
@@ -2566,51 +2736,63 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (!TextUtils.equals(s, typedWord)) {
suggestions.add(new SuggestedWordInfo(s,
SuggestionStripView.MAX_SUGGESTIONS - i,
- SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED));
+ SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
}
}
}
mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
- // TODO: this is in chars but the callee expects code points!
- mWordComposer.setCursorPositionWithinWord(numberOfCharsInWordBeforeCursor);
+ mWordComposer.setCursorPositionWithinWord(
+ typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
mConnection.setComposingRegion(
mLastSelectionStart - numberOfCharsInWordBeforeCursor,
mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
- final SuggestedWords suggestedWords;
if (suggestions.isEmpty()) {
// We come here if there weren't any suggestion spans on this word. We will try to
// compute suggestions for it instead.
- final SuggestedWords suggestedWordsIncludingTypedWord =
- getSuggestedWords(Suggest.SESSION_TYPING);
- if (suggestedWordsIncludingTypedWord.size() > 1) {
- // We were able to compute new suggestions for this word.
- // Remove the typed word, since we don't want to display it in this case.
- // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false.
- suggestedWords =
- suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord();
- } else {
- // No saved suggestions, and we were unable to compute any good one either.
- // Rather than displaying an empty suggestion strip, we'll display the original
- // word alone in the middle.
- // Since there is only one word, willAutoCorrect is false.
- suggestedWords = suggestedWordsIncludingTypedWord;
- }
+ mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
+ new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(
+ final SuggestedWords suggestedWordsIncludingTypedWord) {
+ final SuggestedWords suggestedWords;
+ if (suggestedWordsIncludingTypedWord.size() > 1) {
+ // We were able to compute new suggestions for this word.
+ // Remove the typed word, since we don't want to display it in this case.
+ // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to
+ // false.
+ suggestedWords = suggestedWordsIncludingTypedWord
+ .getSuggestedWordsExcludingTypedWord();
+ } else {
+ // No saved suggestions, and we were unable to compute any good one either.
+ // Rather than displaying an empty suggestion strip, we'll display the
+ // original word alone in the middle.
+ // Since there is only one word, willAutoCorrect is false.
+ suggestedWords = suggestedWordsIncludingTypedWord;
+ }
+ unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords);
+ }});
} else {
// We found suggestion spans in the word. We'll create the SuggestedWords out of
// them, and make willAutoCorrect false.
- suggestedWords = new SuggestedWords(suggestions,
+ final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
true /* typedWordValid */, false /* willAutoCorrect */,
false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
false /* isPrediction */);
+ unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords);
}
+ }
+ public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
+ final SuggestedWords suggestedWords) {
// Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
- // We never want to auto-correct on a resumed suggestion. Please refer to the three
- // places above where suggestedWords is affected. We also need to reset
- // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it.
- // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition)
+ // We never want to auto-correct on a resumed suggestion. Please refer to the three places
+ // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
+ // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
+ // the text to adapt it.
+ // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
mIsAutoCorrectionIndicatorOn = false;
- showSuggestionStrip(suggestedWords, typedWord);
+ mHandler.showSuggestionStrip(suggestedWords);
}
/**
@@ -2666,7 +2848,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord);
}
- mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
+ final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+ if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
+ // For languages with spaces, we revert to the typed string, but the cursor is still
+ // after the separator so we don't resume suggestions. If the user wants to correct
+ // the word, they have to press backspace again.
+ mConnection.commitText(stringToCommit, 1);
+ } else {
+ // For languages without spaces, we revert the typed string but the cursor is flush
+ // with the typed word, so we need to resume suggestions right away.
+ mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
+ mConnection.setComposingText(stringToCommit, 1);
+ }
if (mSettings.isInternal()) {
LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
@@ -2684,7 +2877,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This essentially inserts a space, and that's it.
public void promotePhantomSpace() {
- if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ if (currentSettings.shouldInsertSpacesAutomatically()
+ && currentSettings.mCurrentLanguageHasSpaces
&& !mConnection.textBeforeCursorLooksLikeURL()) {
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_promotePhantomSpace();
@@ -2710,30 +2905,43 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- private void hapticAndAudioFeedback(final int code, final boolean isRepeatKey) {
+ private void hapticAndAudioFeedback(final int code, final int repeatCount) {
final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
// No need to feedback while sliding input.
return;
}
- if (isRepeatKey && code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
- // No need to feedback when repeating delete key will have no effect.
- return;
+ if (repeatCount > 0) {
+ if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
+ // No need to feedback when repeat delete key will have no effect.
+ return;
+ }
+ // TODO: Use event time that the last feedback has been generated instead of relying on
+ // a repeat count to thin out feedback.
+ if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
+ return;
+ }
+ }
+ final AudioAndHapticFeedbackManager feedbackManager =
+ AudioAndHapticFeedbackManager.getInstance();
+ if (repeatCount == 0) {
+ // TODO: Reconsider how to perform haptic feedback when repeating key.
+ feedbackManager.performHapticFeedback(keyboardView);
}
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, keyboardView);
+ feedbackManager.performAudioFeedback(code);
}
// Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
// release matching call is {@link #onReleaseKey(int,boolean)} below.
@Override
- public void onPressKey(final int primaryCode, final boolean isRepeatKey,
+ public void onPressKey(final int primaryCode, final int repeatCount,
final boolean isSinglePointer) {
mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
- hapticAndAudioFeedback(primaryCode, isRepeatKey);
+ hapticAndAudioFeedback(primaryCode, repeatCount);
}
// Callback of the {@link KeyboardActionListener}. This is called when a key is released;
- // press matching call is {@link #onPressKey(int,boolean,boolean)} above.
+ // press matching call is {@link #onPressKey(int,int,boolean)} above.
@Override
public void onReleaseKey(final int primaryCode, final boolean withSliding) {
mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
@@ -2749,17 +2957,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
}
}
-
- if (Constants.CODE_DELETE == primaryCode) {
- // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
- // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
- // pair-friendly way of deleting characters in InputConnection.
- // TODO: use getCodePointBeforeCursor instead to improve performance
- final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
- if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
- mConnection.deleteSurroundingText(1, 0);
- }
- }
}
// Hooks for hardware keyboard
@@ -2895,6 +3092,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mSuggest.hasMainDictionary();
}
+ // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
+ @UsedForTesting
+ /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
+ mSuggest.resetMainDict(this, locale, null);
+ }
+
public void debugDumpStateAndCrashWithException(final String context) {
final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 35920f8cb..a031bb3be 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -169,7 +169,6 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(mComposingText);
- mExpectedCursorPosition += mComposingText.length();
mComposingText.setLength(0);
if (null != mIC) {
mIC.finishComposingText();
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index be03d4ae5..0889f22ca 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -53,12 +53,22 @@ public final class SubtypeSwitcher {
private InputMethodInfo mShortcutInputMethodInfo;
private InputMethodSubtype mShortcutSubtype;
private InputMethodSubtype mNoLanguageSubtype;
+ private InputMethodSubtype mEmojiSubtype;
private boolean mIsNetworkConnected;
// Dummy no language QWERTY subtype. See {@link R.xml.method}.
private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype(
- R.string.subtype_no_language_qwerty, R.drawable.ic_subtype_keyboard, "zz", "keyboard",
- "KeyboardLayoutSet=qwerty,AsciiCapable,EnabledWhenDefaultIsNotAsciiCapable",
+ R.string.subtype_no_language_qwerty, R.drawable.ic_subtype_keyboard,
+ SubtypeLocaleUtils.NO_LANGUAGE, "keyboard",
+ "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
+ + ",AsciiCapable,EnabledWhenDefaultIsNotAsciiCapable",
+ false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
+ // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
+ // Dummy Emoji subtype. See {@link R.xml.method}.
+ private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = new InputMethodSubtype(
+ R.string.subtype_emoji, R.drawable.ic_subtype_keyboard,
+ SubtypeLocaleUtils.NO_LANGUAGE, "keyboard",
+ "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI,
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
static final class NeedsToDisplayLanguage {
@@ -271,4 +281,17 @@ public final class SubtypeSwitcher {
+ DUMMY_NO_LANGUAGE_SUBTYPE);
return DUMMY_NO_LANGUAGE_SUBTYPE;
}
+
+ public InputMethodSubtype getEmojiSubtype() {
+ if (mEmojiSubtype == null) {
+ mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+ }
+ if (mEmojiSubtype != null) {
+ return mEmojiSubtype;
+ }
+ Log.w(TAG, "Can't find Emoji subtype");
+ Log.w(TAG, "No input method subtype found; return dummy subtype: " + DUMMY_EMOJI_SUBTYPE);
+ return DUMMY_EMOJI_SUBTYPE;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index c2fdcb552..18ba15872 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -24,6 +24,7 @@ import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary;
import com.android.inputmethod.latin.settings.Settings;
@@ -107,7 +108,7 @@ public final class Suggest {
}
private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) {
- if (mOnlyDictionarySetForDebug != null && mOnlyDictionarySetForDebug.contains(key)) {
+ if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
return;
}
@@ -200,28 +201,41 @@ public final class Suggest {
personalizationPredictionDictionary);
}
+ public void setPersonalizationDictionary(
+ final PersonalizationDictionary personalizationDictionary) {
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION,
+ personalizationDictionary);
+ }
+
public void setAutoCorrectionThreshold(float threshold) {
mAutoCorrectionThreshold = threshold;
}
- public SuggestedWords getSuggestedWords(final WordComposer wordComposer,
+ public interface OnGetSuggestedWordsCallback {
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords);
+ }
+
+ public void getSuggestedWords(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
- final int sessionId) {
+ final int[] additionalFeaturesOptions, final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
if (wordComposer.isBatchMode()) {
- return getSuggestedWordsForBatchInput(
- wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId);
+ getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions, sessionId, callback);
} else {
- return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
- blockOffensiveWords, isCorrectionEnabled);
+ getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
+ blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions, callback);
}
}
- // Retrieves suggestions for the typing input.
- private SuggestedWords getSuggestedWordsForTypingInput(final WordComposer wordComposer,
+ // Retrieves suggestions for the typing input
+ // and calls the callback function with the suggestions.
+ private void getSuggestedWordsForTypingInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final boolean isCorrectionEnabled) {
+ final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
+ final int[] additionalFeaturesOptions, final OnGetSuggestedWordsCallback callback) {
final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
@@ -244,8 +258,9 @@ public final class Suggest {
for (final String key : mDictionaries.keySet()) {
final Dictionary dictionary = mDictionaries.get(key);
- suggestionsSet.addAll(dictionary.getSuggestions(
- wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords));
+ suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup,
+ prevWordForBigram, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions));
}
final String whitelistedWord;
@@ -303,13 +318,15 @@ public final class Suggest {
for (int i = 0; i < suggestionsCount; ++i) {
final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
- LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(),
+ wordInfo.mSourceDict.mDictType);
}
if (!TextUtils.isEmpty(typedWord)) {
suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
- Dictionary.TYPE_USER_TYPED));
+ Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
}
SuggestedWordInfo.removeDups(suggestionsContainer);
@@ -320,7 +337,7 @@ public final class Suggest {
suggestionsList = suggestionsContainer;
}
- return new SuggestedWords(suggestionsList,
+ callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
// TODO: this first argument is lying. If this is a whitelisted word which is an
// actual word, it says typedWordValid = false, which looks wrong. We should either
// rename the attribute or change the value.
@@ -328,13 +345,15 @@ public final class Suggest {
hasAutoCorrection, /* willAutoCorrect */
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- !wordComposer.isComposingWord() /* isPrediction */);
+ !wordComposer.isComposingWord() /* isPrediction */));
}
- // Retrieves suggestions for the batch input.
- private SuggestedWords getSuggestedWordsForBatchInput(final WordComposer wordComposer,
+ // Retrieves suggestions for the batch input
+ // and calls the callback function with the suggestions.
+ private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int sessionId) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId, final OnGetSuggestedWordsCallback callback) {
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
@@ -348,11 +367,12 @@ public final class Suggest {
}
final Dictionary dictionary = mDictionaries.get(key);
suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
- prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId));
+ prevWordForBigram, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions, sessionId));
}
for (SuggestedWordInfo wordInfo : suggestionsSet) {
- LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType);
}
final ArrayList<SuggestedWordInfo> suggestionsContainer =
@@ -387,12 +407,12 @@ public final class Suggest {
// In the batch input mode, the most relevant suggested word should act as a "typed word"
// (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
- return new SuggestedWords(suggestionsContainer,
+ callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
true /* typedWordValid */,
false /* willAutoCorrect */,
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- false /* isPrediction */);
+ false /* isPrediction */));
}
private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
@@ -453,7 +473,7 @@ public final class Suggest {
sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
}
return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKind,
- wordInfo.mSourceDict);
+ wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord);
}
public void close() {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 22beaefee..b27fd81e9 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -113,7 +113,8 @@ public final class SuggestedWords {
if (null == text) continue;
final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(),
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED,
- Dictionary.TYPE_APPLICATION_DEFINED);
+ Dictionary.DICTIONARY_APPLICATION_DEFINED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */);
result.add(suggestedWordInfo);
}
return result;
@@ -126,7 +127,8 @@ public final class SuggestedWords {
final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList();
final HashSet<String> alreadySeen = CollectionUtils.newHashSet();
suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
- SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED));
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
alreadySeen.add(typedWord.toString());
final int previousSize = previousSuggestions.size();
for (int index = 1; index < previousSize; index++) {
@@ -141,7 +143,14 @@ public final class SuggestedWords {
return suggestionsList;
}
+ public SuggestedWordInfo getAutoCommitCandidate() {
+ if (mSuggestedWordInfoList.size() <= 0) return null;
+ final SuggestedWordInfo candidate = mSuggestedWordInfoList.get(0);
+ return candidate.isEligibleForAutoCommit() ? candidate : null;
+ }
+
public static final class SuggestedWordInfo {
+ public static final int NOT_AN_INDEX = -1;
public static final int MAX_SCORE = Integer.MAX_VALUE;
public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
public static final int KIND_TYPED = 0; // What user typed
@@ -166,18 +175,26 @@ public final class SuggestedWords {
public final int mScore;
public final int mKind; // one of the KIND_* constants above
public final int mCodePointCount;
- public final String mSourceDict;
+ public final Dictionary mSourceDict;
+ // For auto-commit. This keeps track of the index inside the touch coordinates array
+ // passed to native code to get suggestions for a gesture that corresponds to the first
+ // letter of the second word.
+ public final int mIndexOfTouchPointOfSecondWord;
private String mDebugString = "";
public SuggestedWordInfo(final String word, final int score, final int kind,
- final String sourceDict) {
+ final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord) {
mWord = word;
mScore = score;
mKind = kind;
mSourceDict = sourceDict;
mCodePointCount = StringUtils.codePointCount(mWord);
+ mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord;
}
+ public boolean isEligibleForAutoCommit() {
+ return (KIND_CORRECTION == mKind && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
+ }
public void setDebugString(final String str) {
if (null == str) throw new NullPointerException("Debug info is null");
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 92f96c027..67ef538ac 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -34,9 +34,10 @@ public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsB
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 33fe89611..bea522320 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -37,9 +37,10 @@ public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDic
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index ed6fefae4..b2bb61596 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -75,7 +75,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
public UserBinaryDictionary(final Context context, final String locale,
final boolean alsoUseMoreRestrictiveLocales) {
- super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
+ super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER,
+ false /* isUpdatable */);
if (null == locale) throw new NullPointerException(); // Catch the error earlier
if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) {
// If we don't have a locale, insert into the "all locales" user dictionary.
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index a09ca605c..039dadc66 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -272,8 +272,8 @@ public final class WordComposer {
final int x, y;
final Key key;
if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
- x = key.mX + key.mWidth / 2;
- y = key.mY + key.mHeight / 2;
+ x = key.getX() + key.getWidth() / 2;
+ y = key.getY() + key.getHeight() / 2;
} else {
x = Constants.NOT_A_COORDINATE;
y = Constants.NOT_A_COORDINATE;
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
new file mode 100644
index 000000000..ceb8fa81f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -0,0 +1,634 @@
+/*
+ * 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.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Decodes binary files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ *
+ * TODO: Remove calls from classes except Ver3DictDecoder
+ * TODO: Move this file to makedict/internal.
+ * TODO: Rename this class to DictDecoderUtils.
+ */
+public final class BinaryDictDecoderUtils {
+
+ private static final boolean DBG = MakedictLog.DBG;
+
+ private BinaryDictDecoderUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static final int MAX_JUMPS = 12;
+
+ @UsedForTesting
+ public interface DictBuffer {
+ public int readUnsignedByte();
+ public int readUnsignedShort();
+ public int readUnsignedInt24();
+ public int readInt();
+ public int position();
+ public void position(int newPosition);
+ public void put(final byte b);
+ public int limit();
+ @UsedForTesting
+ public int capacity();
+ }
+
+ public static final class ByteBufferDictBuffer implements DictBuffer {
+ private ByteBuffer mBuffer;
+
+ public ByteBufferDictBuffer(final ByteBuffer buffer) {
+ mBuffer = buffer;
+ }
+
+ @Override
+ public int readUnsignedByte() {
+ return mBuffer.get() & 0xFF;
+ }
+
+ @Override
+ public int readUnsignedShort() {
+ return mBuffer.getShort() & 0xFFFF;
+ }
+
+ @Override
+ public int readUnsignedInt24() {
+ final int retval = readUnsignedByte();
+ return (retval << 16) + readUnsignedShort();
+ }
+
+ @Override
+ public int readInt() {
+ return mBuffer.getInt();
+ }
+
+ @Override
+ public int position() {
+ return mBuffer.position();
+ }
+
+ @Override
+ public void position(int newPos) {
+ mBuffer.position(newPos);
+ }
+
+ @Override
+ public void put(final byte b) {
+ mBuffer.put(b);
+ }
+
+ @Override
+ public int limit() {
+ return mBuffer.limit();
+ }
+
+ @Override
+ public int capacity() {
+ return mBuffer.capacity();
+ }
+ }
+
+ /**
+ * A class grouping utility function for our specific character encoding.
+ */
+ static final class CharEncoding {
+ private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+ private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
+
+ /**
+ * Helper method to find out whether this code fits on one byte
+ */
+ private static boolean fitsOnOneByte(final int character) {
+ return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
+ && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
+ }
+
+ /**
+ * Compute the size of a character given its character code.
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * @param character the character code.
+ * @return the size in binary encoded-form, either 1 or 3 bytes.
+ */
+ static int getCharSize(final int character) {
+ // See char encoding in FusionDictionary.java
+ if (fitsOnOneByte(character)) return 1;
+ if (FormatSpec.INVALID_CHARACTER == character) return 1;
+ return 3;
+ }
+
+ /**
+ * Compute the byte size of a character array.
+ */
+ static int getCharArraySize(final int[] chars) {
+ int size = 0;
+ for (int character : chars) size += getCharSize(character);
+ return size;
+ }
+
+ /**
+ * Writes a char array to a byte buffer.
+ *
+ * @param codePoints the code point array to write.
+ * @param buffer the byte buffer to write to.
+ * @param index the index in buffer to write the character array to.
+ * @return the index after the last character.
+ */
+ static int writeCharArray(final int[] codePoints, final byte[] buffer, int index) {
+ for (int codePoint : codePoints) {
+ if (1 == getCharSize(codePoint)) {
+ buffer[index++] = (byte)codePoint;
+ } else {
+ buffer[index++] = (byte)(0xFF & (codePoint >> 16));
+ buffer[index++] = (byte)(0xFF & (codePoint >> 8));
+ buffer[index++] = (byte)(0xFF & codePoint);
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Writes a string with our character format to a byte buffer.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param buffer the byte buffer to write to.
+ * @param origin the offset to write from.
+ * @param word the string to write.
+ * @return the size written, in bytes.
+ */
+ static int writeString(final byte[] buffer, final int origin,
+ final String word) {
+ final int length = word.length();
+ int index = origin;
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ if (1 == getCharSize(codePoint)) {
+ buffer[index++] = (byte)codePoint;
+ } else {
+ buffer[index++] = (byte)(0xFF & (codePoint >> 16));
+ buffer[index++] = (byte)(0xFF & (codePoint >> 8));
+ buffer[index++] = (byte)(0xFF & codePoint);
+ }
+ }
+ buffer[index++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR;
+ return index - origin;
+ }
+
+ /**
+ * Writes a string with our character format to a ByteArrayOutputStream.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param buffer the ByteArrayOutputStream to write to.
+ * @param word the string to write.
+ */
+ static void writeString(final ByteArrayOutputStream buffer, final String word) {
+ final int length = word.length();
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ if (1 == getCharSize(codePoint)) {
+ buffer.write((byte) codePoint);
+ } else {
+ buffer.write((byte) (0xFF & (codePoint >> 16)));
+ buffer.write((byte) (0xFF & (codePoint >> 8)));
+ buffer.write((byte) (0xFF & codePoint));
+ }
+ }
+ buffer.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ }
+
+ /**
+ * Reads a string from a DictBuffer. This is the converse of the above method.
+ */
+ static String readString(final DictBuffer dictBuffer) {
+ final StringBuilder s = new StringBuilder();
+ int character = readChar(dictBuffer);
+ while (character != FormatSpec.INVALID_CHARACTER) {
+ s.appendCodePoint(character);
+ character = readChar(dictBuffer);
+ }
+ return s.toString();
+ }
+
+ /**
+ * Reads a character from the buffer.
+ *
+ * This follows the character format documented earlier in this source file.
+ *
+ * @param dictBuffer the buffer, positioned over an encoded character.
+ * @return the character code.
+ */
+ static int readChar(final DictBuffer dictBuffer) {
+ int character = dictBuffer.readUnsignedByte();
+ if (!fitsOnOneByte(character)) {
+ if (FormatSpec.PTNODE_CHARACTERS_TERMINATOR == character) {
+ return FormatSpec.INVALID_CHARACTER;
+ }
+ character <<= 16;
+ character += dictBuffer.readUnsignedShort();
+ }
+ return character;
+ }
+ }
+
+ // Input methods: Read a binary dictionary to memory.
+ // readDictionaryBinary is the public entry point for them.
+
+ static int readSInt24(final DictBuffer dictBuffer) {
+ final int retval = dictBuffer.readUnsignedInt24();
+ final int sign = ((retval & FormatSpec.MSB24) != 0) ? -1 : 1;
+ return sign * (retval & FormatSpec.SINT24_MAX);
+ }
+
+ static int readChildrenAddress(final DictBuffer dictBuffer,
+ final int optionFlags, final FormatOptions options) {
+ if (options.mSupportsDynamicUpdate) {
+ final int address = dictBuffer.readUnsignedInt24();
+ if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
+ if ((address & FormatSpec.MSB24) != 0) {
+ return -(address & FormatSpec.SINT24_MAX);
+ } else {
+ return address;
+ }
+ }
+ int address;
+ switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
+ return dictBuffer.readUnsignedByte();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES:
+ return dictBuffer.readUnsignedShort();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES:
+ return dictBuffer.readUnsignedInt24();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS:
+ default:
+ return FormatSpec.NO_CHILDREN_ADDRESS;
+ }
+ }
+
+ static int readParentAddress(final DictBuffer dictBuffer,
+ final FormatOptions formatOptions) {
+ if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+ final int parentAddress = dictBuffer.readUnsignedInt24();
+ final int sign = ((parentAddress & FormatSpec.MSB24) != 0) ? -1 : 1;
+ return sign * (parentAddress & FormatSpec.SINT24_MAX);
+ } else {
+ return FormatSpec.NO_PARENT_ADDRESS;
+ }
+ }
+
+ /**
+ * Reads and returns the PtNode count out of a buffer and forwards the pointer.
+ */
+ /* package */ static int readPtNodeCount(final DictBuffer dictBuffer) {
+ final int msb = dictBuffer.readUnsignedByte();
+ if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= msb) {
+ return msb;
+ } else {
+ return ((FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT & msb) << 8)
+ + dictBuffer.readUnsignedByte();
+ }
+ }
+
+ /**
+ * Finds, as a string, the word at the position passed as an argument.
+ *
+ * @param dictDecoder the dict decoder.
+ * @param headerSize the size of the header.
+ * @param pos the position to seek.
+ * @param formatOptions file format options.
+ * @return the word with its frequency, as a weighted string.
+ */
+ /* package for tests */ static WeightedString getWordAtPosition(
+ final Ver3DictDecoder dictDecoder, final int headerSize, final int pos,
+ final FormatOptions formatOptions) {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ final WeightedString result;
+ final int originalPos = dictBuffer.position();
+ dictBuffer.position(pos);
+
+ if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+ result = getWordAtPositionWithParentAddress(dictDecoder, pos, formatOptions);
+ } else {
+ result = getWordAtPositionWithoutParentAddress(dictDecoder, headerSize, pos,
+ formatOptions);
+ }
+
+ dictBuffer.position(originalPos);
+ return result;
+ }
+
+ @SuppressWarnings("unused")
+ private static WeightedString getWordAtPositionWithParentAddress(
+ final Ver3DictDecoder dictDecoder, final int pos, final FormatOptions options) {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ int currentPos = pos;
+ int frequency = Integer.MIN_VALUE;
+ final StringBuilder builder = new StringBuilder();
+ // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH
+ for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) {
+ PtNodeInfo currentInfo;
+ int loopCounter = 0;
+ do {
+ dictBuffer.position(currentPos);
+ currentInfo = dictDecoder.readPtNode(currentPos, options);
+ if (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)) {
+ currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
+ }
+ if (DBG && loopCounter++ > MAX_JUMPS) {
+ MakedictLog.d("Too many jumps - probably a bug");
+ }
+ } while (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options));
+ if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency;
+ builder.insert(0,
+ new String(currentInfo.mCharacters, 0, currentInfo.mCharacters.length));
+ if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break;
+ currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
+ }
+ return new WeightedString(builder.toString(), frequency);
+ }
+
+ private static WeightedString getWordAtPositionWithoutParentAddress(
+ final Ver3DictDecoder dictDecoder, final int headerSize, final int pos,
+ final FormatOptions options) {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ dictBuffer.position(headerSize);
+ final int count = readPtNodeCount(dictBuffer);
+ int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count);
+ final StringBuilder builder = new StringBuilder();
+ WeightedString result = null;
+
+ PtNodeInfo last = null;
+ for (int i = count - 1; i >= 0; --i) {
+ PtNodeInfo info = dictDecoder.readPtNode(groupPos, options);
+ groupPos = info.mEndAddress;
+ if (info.mOriginalAddress == pos) {
+ builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
+ result = new WeightedString(builder.toString(), info.mFrequency);
+ break; // and return
+ }
+ if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) {
+ if (info.mChildrenAddress > pos) {
+ if (null == last) continue;
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ dictBuffer.position(last.mChildrenAddress);
+ i = readPtNodeCount(dictBuffer);
+ groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i);
+ last = null;
+ continue;
+ }
+ last = info;
+ }
+ if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) {
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ dictBuffer.position(last.mChildrenAddress);
+ i = readPtNodeCount(dictBuffer);
+ groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i);
+ last = null;
+ continue;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Reads a single node array from a buffer.
+ *
+ * This methods reads the file at the current position. A node array is fully expected to start
+ * at the current position.
+ * This will recursively read other node arrays into the structure, populating the reverse
+ * maps on the fly and using them to keep track of already read nodes.
+ *
+ * @param dictDecoder the dict decoder, correctly positioned at the start of a node array.
+ * @param headerSize the size, in bytes, of the file header.
+ * @param reverseNodeArrayMap a mapping from addresses to already read node arrays.
+ * @param reversePtNodeMap a mapping from addresses to already read PtNodes.
+ * @param options file format options.
+ * @return the read node array with all his children already read.
+ */
+ private static PtNodeArray readNodeArray(final Ver3DictDecoder dictDecoder,
+ final int headerSize, final Map<Integer, PtNodeArray> reverseNodeArrayMap,
+ final Map<Integer, PtNode> reversePtNodeMap, final FormatOptions options)
+ throws IOException {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ final ArrayList<PtNode> nodeArrayContents = new ArrayList<PtNode>();
+ final int nodeArrayOriginPos = dictBuffer.position();
+
+ do { // Scan the linked-list node.
+ final int nodeArrayHeadPos = dictBuffer.position();
+ final int count = readPtNodeCount(dictBuffer);
+ int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count);
+ for (int i = count; i > 0; --i) { // Scan the array of PtNode.
+ PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options);
+ if (BinaryDictIOUtils.isMovedPtNode(info.mFlags, options)) continue;
+ ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
+ ArrayList<WeightedString> bigrams = null;
+ if (null != info.mBigrams) {
+ bigrams = new ArrayList<WeightedString>();
+ for (PendingAttribute bigram : info.mBigrams) {
+ final WeightedString word = getWordAtPosition(dictDecoder, headerSize,
+ bigram.mAddress, options);
+ final int reconstructedFrequency =
+ BinaryDictIOUtils.reconstructBigramFrequency(word.mFrequency,
+ bigram.mFrequency);
+ bigrams.add(new WeightedString(word.mWord, reconstructedFrequency));
+ }
+ }
+ if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) {
+ PtNodeArray children = reverseNodeArrayMap.get(info.mChildrenAddress);
+ if (null == children) {
+ final int currentPosition = dictBuffer.position();
+ dictBuffer.position(info.mChildrenAddress);
+ children = readNodeArray(dictDecoder, headerSize, reverseNodeArrayMap,
+ reversePtNodeMap, options);
+ dictBuffer.position(currentPosition);
+ }
+ nodeArrayContents.add(
+ new PtNode(info.mCharacters, shortcutTargets, bigrams,
+ info.mFrequency,
+ 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
+ 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children));
+ } else {
+ nodeArrayContents.add(
+ new PtNode(info.mCharacters, shortcutTargets, bigrams,
+ info.mFrequency,
+ 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
+ 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
+ }
+ groupOffsetPos = info.mEndAddress;
+ }
+
+ // reach the end of the array.
+ if (options.mSupportsDynamicUpdate) {
+ final int nextAddress = dictBuffer.readUnsignedInt24();
+ if (nextAddress >= 0 && nextAddress < dictBuffer.limit()) {
+ dictBuffer.position(nextAddress);
+ } else {
+ break;
+ }
+ }
+ } while (options.mSupportsDynamicUpdate &&
+ dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+
+ final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents);
+ nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos;
+ nodeArray.mCachedAddressAfterUpdate = nodeArrayOriginPos;
+ reverseNodeArrayMap.put(nodeArray.mCachedAddressAfterUpdate, nodeArray);
+ return nodeArray;
+ }
+
+ /**
+ * Helper function to get the binary format version from the header.
+ * @throws IOException
+ */
+ private static int getFormatVersion(final DictBuffer dictBuffer)
+ throws IOException {
+ final int magic = dictBuffer.readInt();
+ if (FormatSpec.MAGIC_NUMBER == magic) return dictBuffer.readUnsignedShort();
+ return FormatSpec.NOT_A_VERSION_NUMBER;
+ }
+
+ /**
+ * Helper function to get and validate the binary format version.
+ * @throws UnsupportedFormatException
+ * @throws IOException
+ */
+ static int checkFormatVersion(final DictBuffer dictBuffer)
+ throws IOException, UnsupportedFormatException {
+ final int version = getFormatVersion(dictBuffer);
+ if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
+ || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
+ throw new UnsupportedFormatException("This file has version " + version
+ + ", but this implementation does not support versions above "
+ + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
+ }
+ return version;
+ }
+
+ /**
+ * Reads a buffer and returns the memory representation of the dictionary.
+ *
+ * This high-level method takes a buffer and reads its contents, populating a
+ * FusionDictionary structure. The optional dict argument is an existing dictionary to
+ * which words from the buffer should be added. If it is null, a new dictionary is created.
+ *
+ * @param dictDecoder the dict decoder.
+ * @param dict an optional dictionary to add words to, or null.
+ * @return the created (or merged) dictionary.
+ */
+ @UsedForTesting
+ /* package */ static FusionDictionary readDictionaryBinary(final Ver3DictDecoder dictDecoder,
+ final FusionDictionary dict) throws IOException, UnsupportedFormatException {
+ // Read header
+ final FileHeader fileHeader = dictDecoder.readHeader();
+
+ Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>();
+ Map<Integer, PtNode> reversePtNodeMapping = new TreeMap<Integer, PtNode>();
+ final PtNodeArray root = readNodeArray(dictDecoder, fileHeader.mHeaderSize,
+ reverseNodeArrayMapping, reversePtNodeMapping, fileHeader.mFormatOptions);
+
+ FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions);
+ if (null != dict) {
+ for (final Word w : dict) {
+ if (w.mIsBlacklistEntry) {
+ newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord);
+ } else {
+ newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord);
+ }
+ }
+ for (final Word w : dict) {
+ // By construction a binary dictionary may not have bigrams pointing to
+ // words that are not also registered as unigrams so we don't have to avoid
+ // them explicitly here.
+ for (final WeightedString bigram : w.mBigrams) {
+ newDict.setBigram(w.mWord, bigram.mWord, bigram.mFrequency);
+ }
+ }
+ }
+
+ return newDict;
+ }
+
+ /**
+ * Helper method to pass a file name instead of a File object to isBinaryDictionary.
+ */
+ public static boolean isBinaryDictionary(final String filename) {
+ final File file = new File(filename);
+ return isBinaryDictionary(file);
+ }
+
+ /**
+ * Basic test to find out whether the file is a binary dictionary or not.
+ *
+ * Concretely this only tests the magic number.
+ *
+ * @param file The file to test.
+ * @return true if it's a binary dictionary, false otherwise
+ */
+ public static boolean isBinaryDictionary(final File file) {
+ FileInputStream inStream = null;
+ try {
+ inStream = new FileInputStream(file);
+ final ByteBuffer buffer = inStream.getChannel().map(
+ FileChannel.MapMode.READ_ONLY, 0, file.length());
+ final int version = getFormatVersion(new ByteBufferDictBuffer(buffer));
+ return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION
+ && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION);
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
new file mode 100644
index 000000000..79f5ad8bd
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -0,0 +1,1007 @@
+/*
+ * 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.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Encodes binary files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ *
+ * TODO: Rename this class to DictEncoderUtils.
+ */
+public class BinaryDictEncoderUtils {
+
+ private static final boolean DBG = MakedictLog.DBG;
+
+ private BinaryDictEncoderUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ // Arbitrary limit to how much passes we consider address size compression should
+ // terminate in. At the time of this writing, our largest dictionary completes
+ // compression in five passes.
+ // If the number of passes exceeds this number, makedict bails with an exception on
+ // suspicion that a bug might be causing an infinite loop.
+ private static final int MAX_PASSES = 24;
+
+ /**
+ * Compute the binary size of the character array.
+ *
+ * If only one character, this is the size of this character. If many, it's the sum of their
+ * sizes + 1 byte for the terminator.
+ *
+ * @param characters the character array
+ * @return the size of the char array, including the terminator if any
+ */
+ static int getPtNodeCharactersSize(final int[] characters) {
+ int size = CharEncoding.getCharArraySize(characters);
+ if (characters.length > 1) size += FormatSpec.PTNODE_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the binary size of the character array in a PtNode
+ *
+ * If only one character, this is the size of this character. If many, it's the sum of their
+ * sizes + 1 byte for the terminator.
+ *
+ * @param ptNode the PtNode
+ * @return the size of the char array, including the terminator if any
+ */
+ private static int getPtNodeCharactersSize(final PtNode ptNode) {
+ return getPtNodeCharactersSize(ptNode.mChars);
+ }
+
+ /**
+ * Compute the binary size of the PtNode count for a node array.
+ * @param nodeArray the nodeArray
+ * @return the size of the PtNode count, either 1 or 2 bytes.
+ */
+ private static int getPtNodeCountSize(final PtNodeArray nodeArray) {
+ return BinaryDictIOUtils.getPtNodeCountSize(nodeArray.mData.size());
+ }
+
+ /**
+ * Compute the size of a shortcut in bytes.
+ */
+ private static int getShortcutSize(final WeightedString shortcut) {
+ int size = FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE;
+ final String word = shortcut.mWord;
+ final int length = word.length();
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ size += CharEncoding.getCharSize(codePoint);
+ }
+ size += FormatSpec.PTNODE_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the size of a shortcut list in bytes.
+ *
+ * This is known in advance and does not change according to position in the file
+ * like address lists do.
+ */
+ static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) {
+ if (null == shortcutList || shortcutList.isEmpty()) return 0;
+ int size = FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE;
+ for (final WeightedString shortcut : shortcutList) {
+ size += getShortcutSize(shortcut);
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a PtNode, assuming 3-byte addresses for everything.
+ *
+ * @param ptNode the PtNode to compute the size of.
+ * @param options file format options.
+ * @return the maximum size of the PtNode.
+ */
+ private static int getPtNodeMaximumSize(final PtNode ptNode, final FormatOptions options) {
+ int size = getNodeHeaderSize(ptNode, options);
+ // If terminal, one byte for the frequency
+ if (ptNode.isTerminal()) size += FormatSpec.PTNODE_FREQUENCY_SIZE;
+ size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address
+ size += getShortcutListSize(ptNode.mShortcutTargets);
+ if (null != ptNode.mBigrams) {
+ size += (FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE
+ + FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE)
+ * ptNode.mBigrams.size();
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of each PtNode of a PtNode array, assuming 3-byte addresses for
+ * everything, and caches it in the `mCachedSize' member of the nodes; deduce the size of
+ * the containing node array, and cache it it its 'mCachedSize' member.
+ *
+ * @param ptNodeArray the node array to compute the maximum size of.
+ * @param options file format options.
+ */
+ private static void calculatePtNodeArrayMaximumSize(final PtNodeArray ptNodeArray,
+ final FormatOptions options) {
+ int size = getPtNodeCountSize(ptNodeArray);
+ for (PtNode node : ptNodeArray.mData) {
+ final int nodeSize = getPtNodeMaximumSize(node, options);
+ node.mCachedSize = nodeSize;
+ size += nodeSize;
+ }
+ if (options.mSupportsDynamicUpdate) {
+ size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ }
+ ptNodeArray.mCachedSize = size;
+ }
+
+ /**
+ * Compute the size of the header (flag + [parent address] + characters size) of a PtNode.
+ *
+ * @param ptNode the PtNode of which to compute the size of the header
+ * @param options file format options.
+ */
+ private static int getNodeHeaderSize(final PtNode ptNode, final FormatOptions options) {
+ if (BinaryDictIOUtils.supportsDynamicUpdate(options)) {
+ return FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
+ + getPtNodeCharactersSize(ptNode);
+ } else {
+ return FormatSpec.PTNODE_FLAGS_SIZE + getPtNodeCharactersSize(ptNode);
+ }
+ }
+
+ /**
+ * Compute the size, in bytes, that an address will occupy.
+ *
+ * This can be used either for children addresses (which are always positive) or for
+ * attribute, which may be positive or negative but
+ * store their sign bit separately.
+ *
+ * @param address the address
+ * @return the byte size.
+ */
+ static int getByteSize(final int address) {
+ assert(address <= FormatSpec.UINT24_MAX);
+ if (!BinaryDictIOUtils.hasChildrenAddress(address)) {
+ return 0;
+ } else if (Math.abs(address) <= FormatSpec.UINT8_MAX) {
+ return 1;
+ } else if (Math.abs(address) <= FormatSpec.UINT16_MAX) {
+ return 2;
+ } else {
+ return 3;
+ }
+ }
+
+ // End utility methods
+
+ // This method is responsible for finding a nice ordering of the nodes that favors run-time
+ // cache performance and dictionary size.
+ /* package for tests */ static ArrayList<PtNodeArray> flattenTree(
+ final PtNodeArray rootNodeArray) {
+ final int treeSize = FusionDictionary.countPtNodes(rootNodeArray);
+ MakedictLog.i("Counted nodes : " + treeSize);
+ final ArrayList<PtNodeArray> flatTree = new ArrayList<PtNodeArray>(treeSize);
+ return flattenTreeInner(flatTree, rootNodeArray);
+ }
+
+ private static ArrayList<PtNodeArray> flattenTreeInner(final ArrayList<PtNodeArray> list,
+ final PtNodeArray ptNodeArray) {
+ // Removing the node is necessary if the tails are merged, because we would then
+ // add the same node several times when we only want it once. A number of places in
+ // the code also depends on any node being only once in the list.
+ // Merging tails can only be done if there are no attributes. Searching for attributes
+ // in LatinIME code depends on a total breadth-first ordering, which merging tails
+ // breaks. If there are no attributes, it should be fine (and reduce the file size)
+ // to merge tails, and removing the node from the list would be necessary. However,
+ // we don't merge tails because breaking the breadth-first ordering would result in
+ // extreme overhead at bigram lookup time (it would make the search function O(n) instead
+ // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty
+ // high).
+ // If no nodes are ever merged, we can't have the same node twice in the list, hence
+ // searching for duplicates in unnecessary. It is also very performance consuming,
+ // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making
+ // this simple list.remove operation O(n*n) overall. On Android this overhead is very
+ // high.
+ // 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);
+ }
+ return list;
+ }
+
+ /**
+ * Get the offset from a position inside a current node array to a target node array, during
+ * update.
+ *
+ * If the current node array is before the target node array, the target node array has not
+ * been updated yet, so we should return the offset from the old position of the current node
+ * array to the old position of the target node array. If on the other hand the target is
+ * before the current node array, it already has been updated, so we should return the offset
+ * from the new position in the current node array to the new position in the target node
+ * array.
+ *
+ * @param currentNodeArray node array containing the PtNode where the offset will be written
+ * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray
+ * @param targetNodeArray the target node array to get the offset to
+ * @return the offset to the target node array
+ */
+ private static int getOffsetToTargetNodeArrayDuringUpdate(final PtNodeArray currentNodeArray,
+ final int offsetFromStartOfCurrentNodeArray, final PtNodeArray targetNodeArray) {
+ final boolean isTargetBeforeCurrent = (targetNodeArray.mCachedAddressBeforeUpdate
+ < currentNodeArray.mCachedAddressBeforeUpdate);
+ if (isTargetBeforeCurrent) {
+ return targetNodeArray.mCachedAddressAfterUpdate
+ - (currentNodeArray.mCachedAddressAfterUpdate
+ + offsetFromStartOfCurrentNodeArray);
+ } else {
+ return targetNodeArray.mCachedAddressBeforeUpdate
+ - (currentNodeArray.mCachedAddressBeforeUpdate
+ + offsetFromStartOfCurrentNodeArray);
+ }
+ }
+
+ /**
+ * Get the offset from a position inside a current node array to a target PtNode, during
+ * update.
+ *
+ * @param currentNodeArray node array containing the PtNode where the offset will be written
+ * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray
+ * @param targetPtNode the target PtNode to get the offset to
+ * @return the offset to the target PtNode
+ */
+ // TODO: is there any way to factorize this method with the one above?
+ private static int getOffsetToTargetPtNodeDuringUpdate(final PtNodeArray currentNodeArray,
+ final int offsetFromStartOfCurrentNodeArray, final PtNode targetPtNode) {
+ final int oldOffsetBasePoint = currentNodeArray.mCachedAddressBeforeUpdate
+ + offsetFromStartOfCurrentNodeArray;
+ final boolean isTargetBeforeCurrent = (targetPtNode.mCachedAddressBeforeUpdate
+ < oldOffsetBasePoint);
+ // If the target is before the current node array, then its address has already been
+ // updated. We can use the AfterUpdate member, and compare it to our own member after
+ // update. Otherwise, the AfterUpdate member is not updated yet, so we need to use the
+ // BeforeUpdate member, and of course we have to compare this to our own address before
+ // update.
+ if (isTargetBeforeCurrent) {
+ final int newOffsetBasePoint = currentNodeArray.mCachedAddressAfterUpdate
+ + offsetFromStartOfCurrentNodeArray;
+ return targetPtNode.mCachedAddressAfterUpdate - newOffsetBasePoint;
+ } else {
+ return targetPtNode.mCachedAddressBeforeUpdate - oldOffsetBasePoint;
+ }
+ }
+
+ /**
+ * Computes the actual node array size, based on the cached addresses of the children nodes.
+ *
+ * Each node array stores its tentative address. During dictionary address computing, these
+ * are not final, but they can be used to compute the node array size (the node array size
+ * depends on the address of the children because the number of bytes necessary to store an
+ * address depends on its numeric value. The return value indicates whether the node array
+ * contents (as in, any of the addresses stored in the cache fields) have changed with
+ * respect to their previous value.
+ *
+ * @param ptNodeArray the node array to compute the size of.
+ * @param dict the dictionary in which the word/attributes are to be found.
+ * @param formatOptions file format options.
+ * @return false if none of the cached addresses inside the node array changed, true otherwise.
+ */
+ private static boolean computeActualPtNodeArraySize(final PtNodeArray ptNodeArray,
+ final FusionDictionary dict, final FormatOptions formatOptions) {
+ boolean changed = false;
+ int size = getPtNodeCountSize(ptNodeArray);
+ for (PtNode ptNode : ptNodeArray.mData) {
+ ptNode.mCachedAddressAfterUpdate = ptNodeArray.mCachedAddressAfterUpdate + size;
+ if (ptNode.mCachedAddressAfterUpdate != ptNode.mCachedAddressBeforeUpdate) {
+ changed = true;
+ }
+ int nodeSize = getNodeHeaderSize(ptNode, formatOptions);
+ if (ptNode.isTerminal()) nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE;
+ if (formatOptions.mSupportsDynamicUpdate) {
+ nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
+ } else if (null != ptNode.mChildren) {
+ nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray,
+ 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_FLAGS_SIZE,
+ FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord));
+ nodeSize += getByteSize(offset) + FormatSpec.PTNODE_FLAGS_SIZE;
+ }
+ }
+ ptNode.mCachedSize = nodeSize;
+ size += nodeSize;
+ }
+ if (formatOptions.mSupportsDynamicUpdate) {
+ size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ }
+ if (ptNodeArray.mCachedSize != size) {
+ ptNodeArray.mCachedSize = size;
+ changed = true;
+ }
+ return changed;
+ }
+
+ /**
+ * Initializes the cached addresses of node arrays and their containing nodes from their size.
+ *
+ * @param flatNodes the list of node arrays.
+ * @param formatOptions file format options.
+ * @return the byte size of the entire stack.
+ */
+ private static int initializePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes,
+ final FormatOptions formatOptions) {
+ int nodeArrayOffset = 0;
+ for (final PtNodeArray nodeArray : flatNodes) {
+ nodeArray.mCachedAddressBeforeUpdate = nodeArrayOffset;
+ int nodeCountSize = getPtNodeCountSize(nodeArray);
+ int nodeffset = 0;
+ for (final PtNode ptNode : nodeArray.mData) {
+ ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate =
+ nodeCountSize + nodeArrayOffset + nodeffset;
+ nodeffset += ptNode.mCachedSize;
+ }
+ final int nodeSize = nodeCountSize + nodeffset
+ + (formatOptions.mSupportsDynamicUpdate
+ ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
+ nodeArrayOffset += nodeArray.mCachedSize;
+ }
+ return nodeArrayOffset;
+ }
+
+ /**
+ * Updates the cached addresses of node arrays after recomputing their new positions.
+ *
+ * @param flatNodes the list of node arrays.
+ */
+ private static void updatePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes) {
+ for (final PtNodeArray nodeArray : flatNodes) {
+ nodeArray.mCachedAddressBeforeUpdate = nodeArray.mCachedAddressAfterUpdate;
+ for (final PtNode ptNode : nodeArray.mData) {
+ ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate;
+ }
+ }
+ }
+
+ /**
+ * Compute the cached parent addresses after all has been updated.
+ *
+ * The parent addresses are used by some binary formats at write-to-disk time. Not all formats
+ * need them. In particular, version 2 does not need them, and version 3 does.
+ *
+ * @param flatNodes the flat array of node arrays to fill in
+ */
+ private static void computeParentAddresses(final ArrayList<PtNodeArray> flatNodes) {
+ for (final PtNodeArray nodeArray : flatNodes) {
+ for (final PtNode ptNode : nodeArray.mData) {
+ if (null != ptNode.mChildren) {
+ // Assign my address to children's parent address
+ // Here BeforeUpdate and AfterUpdate addresses have the same value, so it
+ // does not matter which we use.
+ ptNode.mChildren.mCachedParentAddress = ptNode.mCachedAddressAfterUpdate
+ - ptNode.mChildren.mCachedAddressAfterUpdate;
+ }
+ }
+ }
+ }
+
+ /**
+ * Compute the addresses and sizes of an ordered list of PtNode arrays.
+ *
+ * This method takes a list of PtNode arrays and will update their cached address and size
+ * values so that they can be written into a file. It determines the smallest size each of the
+ * PtNode arrays can be given the addresses of its children and attributes, and store that into
+ * each PtNode.
+ * The order of the PtNode is given by the order of the array. This method makes no effort
+ * to find a good order; it only mechanically computes the size this order results in.
+ *
+ * @param dict the dictionary
+ * @param flatNodes the ordered list of PtNode arrays
+ * @param formatOptions file format options.
+ * @return the same array it was passed. The nodes have been updated for address and size.
+ */
+ private static ArrayList<PtNodeArray> computeAddresses(final FusionDictionary dict,
+ final ArrayList<PtNodeArray> flatNodes, final FormatOptions formatOptions) {
+ // First get the worst possible sizes and offsets
+ for (final PtNodeArray n : flatNodes) calculatePtNodeArrayMaximumSize(n, formatOptions);
+ final int offset = initializePtNodeArraysCachedAddresses(flatNodes, formatOptions);
+
+ MakedictLog.i("Compressing the array addresses. Original size : " + offset);
+ MakedictLog.i("(Recursively seen size : " + offset + ")");
+
+ int passes = 0;
+ boolean changesDone = false;
+ do {
+ changesDone = false;
+ int ptNodeArrayStartOffset = 0;
+ for (final PtNodeArray ptNodeArray : flatNodes) {
+ ptNodeArray.mCachedAddressAfterUpdate = ptNodeArrayStartOffset;
+ final int oldNodeArraySize = ptNodeArray.mCachedSize;
+ final boolean changed =
+ computeActualPtNodeArraySize(ptNodeArray, dict, formatOptions);
+ final int newNodeArraySize = ptNodeArray.mCachedSize;
+ if (oldNodeArraySize < newNodeArraySize) {
+ throw new RuntimeException("Increased size ?!");
+ }
+ ptNodeArrayStartOffset += newNodeArraySize;
+ changesDone |= changed;
+ }
+ updatePtNodeArraysCachedAddresses(flatNodes);
+ ++passes;
+ if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
+ } while (changesDone);
+
+ if (formatOptions.mSupportsDynamicUpdate) {
+ computeParentAddresses(flatNodes);
+ }
+ final PtNodeArray lastPtNodeArray = flatNodes.get(flatNodes.size() - 1);
+ MakedictLog.i("Compression complete in " + passes + " passes.");
+ MakedictLog.i("After address compression : "
+ + (lastPtNodeArray.mCachedAddressAfterUpdate + lastPtNodeArray.mCachedSize));
+
+ return flatNodes;
+ }
+
+ /**
+ * Sanity-checking method.
+ *
+ * This method checks a list of PtNode arrays for juxtaposition, that is, it will do
+ * nothing if each node array's cached address is actually the previous node array's address
+ * plus the previous node's size.
+ * If this is not the case, it will throw an exception.
+ *
+ * @param arrays the list of node arrays to check
+ */
+ private static void checkFlatPtNodeArrayList(final ArrayList<PtNodeArray> arrays) {
+ int offset = 0;
+ int index = 0;
+ for (final PtNodeArray ptNodeArray : arrays) {
+ // BeforeUpdate and AfterUpdate addresses are the same here, so it does not matter
+ // which we use.
+ if (ptNodeArray.mCachedAddressAfterUpdate != offset) {
+ throw new RuntimeException("Wrong address for node " + index
+ + " : expected " + offset + ", got " +
+ ptNodeArray.mCachedAddressAfterUpdate);
+ }
+ ++index;
+ offset += ptNodeArray.mCachedSize;
+ }
+ }
+
+ /**
+ * Helper method to write a variable-size address to a file.
+ *
+ * @param buffer the buffer to write to.
+ * @param index the index in the buffer to write the address to.
+ * @param address the address to write.
+ * @return the size in bytes the address actually took.
+ */
+ private static int writeVariableAddress(final byte[] buffer, int index, final int address) {
+ switch (getByteSize(address)) {
+ case 1:
+ buffer[index++] = (byte)address;
+ return 1;
+ case 2:
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 2;
+ case 3:
+ buffer[index++] = (byte)(0xFF & (address >> 16));
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 3;
+ case 0:
+ return 0;
+ default:
+ throw new RuntimeException("Address " + address + " has a strange size");
+ }
+ }
+
+ /**
+ * Helper method to write a variable-size signed address to a file.
+ *
+ * @param buffer the buffer to write to.
+ * @param index the index in the buffer to write the address to.
+ * @param address the address to write.
+ * @return the size in bytes the address actually took.
+ */
+ private static int writeVariableSignedAddress(final byte[] buffer, int index,
+ final int address) {
+ if (!BinaryDictIOUtils.hasChildrenAddress(address)) {
+ buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
+ } else {
+ final int absAddress = Math.abs(address);
+ buffer[index++] =
+ (byte)((address < 0 ? FormatSpec.MSB8 : 0) | (0xFF & (absAddress >> 16)));
+ buffer[index++] = (byte)(0xFF & (absAddress >> 8));
+ buffer[index++] = (byte)(0xFF & absAddress);
+ }
+ return 3;
+ }
+
+ /**
+ * Makes the flag value for a PtNode.
+ *
+ * @param hasMultipleChars whether the PtNode has multiple chars.
+ * @param isTerminal whether the PtNode is terminal.
+ * @param childrenAddressSize the size of a children address.
+ * @param hasShortcuts whether the PtNode has shortcuts.
+ * @param hasBigrams whether the PtNode has bigrams.
+ * @param isNotAWord whether the PtNode is not a word.
+ * @param isBlackListEntry whether the PtNode is a blacklist entry.
+ * @param formatOptions file format options.
+ * @return the flags
+ */
+ static int makePtNodeFlags(final boolean hasMultipleChars, final boolean isTerminal,
+ final int childrenAddressSize, final boolean hasShortcuts, final boolean hasBigrams,
+ final boolean isNotAWord, final boolean isBlackListEntry,
+ final FormatOptions formatOptions) {
+ byte flags = 0;
+ if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS;
+ if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL;
+ if (formatOptions.mSupportsDynamicUpdate) {
+ flags |= FormatSpec.FLAG_IS_NOT_MOVED;
+ } else if (true) {
+ switch (childrenAddressSize) {
+ case 1:
+ flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES;
+ break;
+ case 0:
+ flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS;
+ break;
+ default:
+ throw new RuntimeException("Node with a strange address");
+ }
+ }
+ if (hasShortcuts) flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS;
+ if (hasBigrams) flags |= FormatSpec.FLAG_HAS_BIGRAMS;
+ if (isNotAWord) flags |= FormatSpec.FLAG_IS_NOT_A_WORD;
+ if (isBlackListEntry) flags |= FormatSpec.FLAG_IS_BLACKLISTED;
+ return flags;
+ }
+
+ private static byte makePtNodeFlags(final PtNode node, final int ptNodeAddress,
+ final int childrenOffset, final FormatOptions formatOptions) {
+ return (byte) makePtNodeFlags(node.mChars.length > 1, node.mFrequency >= 0,
+ getByteSize(childrenOffset),
+ node.mShortcutTargets != null && !node.mShortcutTargets.isEmpty(),
+ node.mBigrams != null, node.mIsNotAWord, node.mIsBlacklistEntry, formatOptions);
+ }
+
+ /**
+ * Makes the flag value for a bigram.
+ *
+ * @param more whether there are more bigrams after this one.
+ * @param offset the offset of the bigram.
+ * @param bigramFrequency the frequency of the bigram, 0..255.
+ * @param unigramFrequency the unigram frequency of the same word, 0..255.
+ * @param word the second bigram, for debugging purposes
+ * @return the flags
+ */
+ private static final int makeBigramFlags(final boolean more, final int offset,
+ int bigramFrequency, final int unigramFrequency, final String word) {
+ int bigramFlags = (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0)
+ + (offset < 0 ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0);
+ switch (getByteSize(offset)) {
+ case 1:
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Strange offset size");
+ }
+ if (unigramFrequency > bigramFrequency) {
+ MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word
+ + "\". Bigram freq is " + bigramFrequency + ", unigram freq for "
+ + word + " is " + unigramFrequency);
+ bigramFrequency = unigramFrequency;
+ }
+ // We compute the difference between 255 (which means probability = 1) and the
+ // unigram score. We split this into a number of discrete steps.
+ // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15
+ // represents an increase of 16 steps: a value of 15 will be interpreted as the median
+ // value of the 16th step. In all justice, if the bigram frequency is low enough to be
+ // rounded below the first step (which means it is less than half a step higher than the
+ // unigram frequency) then the unigram frequency itself is the best approximation of the
+ // bigram freq that we could possibly supply, hence we should *not* include this bigram
+ // in the file at all.
+ // until this is done, we'll write 0 and slightly overestimate this case.
+ // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step
+ // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to
+ // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the
+ // step size. Then we compute the start of the first step (the one where value 0 starts)
+ // by adding half-a-step to the unigramFrequency. From there, we compute the integer
+ // number of steps to the bigramFrequency. One last thing: we want our steps to include
+ // their lower bound and exclude their higher bound so we need to have the first step
+ // start at exactly 1 unit higher than floor(unigramFreq + half a step).
+ // Note : to reconstruct the score, the dictionary reader will need to divide
+ // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step,
+ // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best
+ // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the
+ // step pointed by the discretized frequency.
+ final float stepSize =
+ (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
+ / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY);
+ final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
+ final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
+ // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
+ // here. The best approximation would be the unigram freq itself, so we should not
+ // include this bigram in the dictionary. For now, register as 0, and live with the
+ // small over-estimation that we get in this case. TODO: actually remove this bigram
+ // if discretizedFrequency < 0.
+ final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0;
+ bigramFlags += finalBigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
+ return bigramFlags;
+ }
+
+ /**
+ * Makes the 2-byte value for options flags.
+ */
+ private static final int makeOptionsValue(final FusionDictionary dictionary,
+ final FormatOptions formatOptions) {
+ final DictionaryOptions options = dictionary.mOptions;
+ final boolean hasBigrams = dictionary.hasBigrams();
+ return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0)
+ + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0)
+ + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0)
+ + (formatOptions.mSupportsDynamicUpdate ? FormatSpec.SUPPORTS_DYNAMIC_UPDATE : 0);
+ }
+
+ /**
+ * Makes the flag value for a shortcut.
+ *
+ * @param more whether there are more attributes after this one.
+ * @param frequency the frequency of the attribute, 0..15
+ * @return the flags
+ */
+ static final int makeShortcutFlags(final boolean more, final int frequency) {
+ return (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0)
+ + (frequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY);
+ }
+
+ private static final int writeParentAddress(final byte[] buffer, final int index,
+ final int address, final FormatOptions formatOptions) {
+ if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+ if (address == FormatSpec.NO_PARENT_ADDRESS) {
+ buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
+ } else {
+ final int absAddress = Math.abs(address);
+ assert(absAddress <= FormatSpec.SINT24_MAX);
+ buffer[index] = (byte)((address < 0 ? FormatSpec.MSB8 : 0)
+ | ((absAddress >> 16) & 0xFF));
+ buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF);
+ buffer[index + 2] = (byte)(absAddress & 0xFF);
+ }
+ return index + 3;
+ } else {
+ return index;
+ }
+ }
+
+ /**
+ * Write a PtNodeArray to memory. The PtNodeArray is expected to have its final position cached.
+ *
+ * @param dict the dictionary the node array is a part of (for relative offsets).
+ * @param buffer the memory buffer to write to.
+ * @param ptNodeArray the node array to write.
+ * @param formatOptions file format options.
+ * @return the address of the END of the node.
+ */
+ @SuppressWarnings("unused")
+ private static int writePlacedNode(final FusionDictionary dict, byte[] buffer,
+ final PtNodeArray ptNodeArray, final FormatOptions formatOptions) {
+ // TODO: Make the code in common with BinaryDictIOUtils#writePtNode
+ int index = ptNodeArray.mCachedAddressAfterUpdate;
+
+ final int ptNodeCount = ptNodeArray.mData.size();
+ final int countSize = getPtNodeCountSize(ptNodeArray);
+ final int parentAddress = ptNodeArray.mCachedParentAddress;
+ if (1 == countSize) {
+ buffer[index++] = (byte)ptNodeCount;
+ } else if (2 == countSize) {
+ // We need to signal 2-byte size by setting the top bit of the MSB to 1, so
+ // we | 0x80 to do this.
+ buffer[index++] = (byte)((ptNodeCount >> 8) | 0x80);
+ buffer[index++] = (byte)(ptNodeCount & 0xFF);
+ } else {
+ throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
+ }
+ int ptNodeAddress = index;
+ for (int i = 0; i < ptNodeCount; ++i) {
+ final PtNode ptNode = ptNodeArray.mData.get(i);
+ if (index != ptNode.mCachedAddressAfterUpdate) {
+ throw new RuntimeException("Bug: write index is not the same as the cached address "
+ + "of the node : " + index + " <> " + ptNode.mCachedAddressAfterUpdate);
+ }
+ ptNodeAddress += getNodeHeaderSize(ptNode, formatOptions);
+ // Sanity checks.
+ if (DBG && ptNode.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) {
+ throw new RuntimeException("A node has a frequency > "
+ + FormatSpec.MAX_TERMINAL_FREQUENCY
+ + " : " + ptNode.mFrequency);
+ }
+ if (ptNode.mFrequency >= 0) ptNodeAddress += FormatSpec.PTNODE_FREQUENCY_SIZE;
+ final int childrenOffset = null == ptNode.mChildren
+ ? FormatSpec.NO_CHILDREN_ADDRESS
+ : ptNode.mChildren.mCachedAddressAfterUpdate - ptNodeAddress;
+ buffer[index++] =
+ makePtNodeFlags(ptNode, ptNodeAddress, childrenOffset, formatOptions);
+
+ if (parentAddress == FormatSpec.NO_PARENT_ADDRESS) {
+ index = writeParentAddress(buffer, index, parentAddress, formatOptions);
+ } else {
+ index = writeParentAddress(buffer, index, parentAddress
+ + (ptNodeArray.mCachedAddressAfterUpdate
+ - ptNode.mCachedAddressAfterUpdate),
+ formatOptions);
+ }
+
+ index = CharEncoding.writeCharArray(ptNode.mChars, buffer, index);
+ if (ptNode.hasSeveralChars()) {
+ buffer[index++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR;
+ }
+ if (ptNode.mFrequency >= 0) {
+ buffer[index++] = (byte) ptNode.mFrequency;
+ }
+
+ final int shift;
+ if (formatOptions.mSupportsDynamicUpdate) {
+ shift = writeVariableSignedAddress(buffer, index, childrenOffset);
+ } else {
+ shift = writeVariableAddress(buffer, index, childrenOffset);
+ }
+ index += shift;
+ ptNodeAddress += shift;
+
+ // Write shortcuts
+ if (null != ptNode.mShortcutTargets && !ptNode.mShortcutTargets.isEmpty()) {
+ final int indexOfShortcutByteSize = index;
+ index += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE;
+ ptNodeAddress += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE;
+ final Iterator<WeightedString> shortcutIterator =
+ ptNode.mShortcutTargets.iterator();
+ while (shortcutIterator.hasNext()) {
+ final WeightedString target = shortcutIterator.next();
+ ++ptNodeAddress;
+ int shortcutFlags = makeShortcutFlags(shortcutIterator.hasNext(),
+ target.mFrequency);
+ buffer[index++] = (byte)shortcutFlags;
+ final int shortcutShift = CharEncoding.writeString(buffer, index, target.mWord);
+ index += shortcutShift;
+ ptNodeAddress += shortcutShift;
+ }
+ final int shortcutByteSize = index - indexOfShortcutByteSize;
+ if (shortcutByteSize > 0xFFFF) {
+ throw new RuntimeException("Shortcut list too large");
+ }
+ buffer[indexOfShortcutByteSize] = (byte)(shortcutByteSize >> 8);
+ buffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF);
+ }
+ // Write bigrams
+ if (null != ptNode.mBigrams) {
+ final Iterator<WeightedString> bigramIterator = ptNode.mBigrams.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;
+ ++ptNodeAddress;
+ final int offset = addressOfBigram - ptNodeAddress;
+ int bigramFlags = makeBigramFlags(bigramIterator.hasNext(), offset,
+ bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
+ buffer[index++] = (byte)bigramFlags;
+ final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
+ index += bigramShift;
+ ptNodeAddress += bigramShift;
+ }
+ }
+
+ }
+ if (formatOptions.mSupportsDynamicUpdate) {
+ buffer[index] = buffer[index + 1] = buffer[index + 2]
+ = FormatSpec.NO_FORWARD_LINK_ADDRESS;
+ index += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ }
+ if (index != ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize) {
+ throw new RuntimeException(
+ "Not the same size : written " + (index - ptNodeArray.mCachedAddressAfterUpdate)
+ + " bytes from a node that should have " + ptNodeArray.mCachedSize + " bytes");
+ }
+ return index;
+ }
+
+ /**
+ * Dumps a collection of useful statistics about a list of PtNode arrays.
+ *
+ * This prints purely informative stuff, like the total estimated file size, the
+ * number of PtNode arrays, of PtNodes, the repartition of each address size, etc
+ *
+ * @param ptNodeArrays the list of PtNode arrays.
+ */
+ private static void showStatistics(ArrayList<PtNodeArray> ptNodeArrays) {
+ int firstTerminalAddress = Integer.MAX_VALUE;
+ int lastTerminalAddress = Integer.MIN_VALUE;
+ int size = 0;
+ int ptNodes = 0;
+ int maxNodes = 0;
+ int maxRuns = 0;
+ for (final PtNodeArray ptNodeArray : ptNodeArrays) {
+ if (maxNodes < ptNodeArray.mData.size()) maxNodes = ptNodeArray.mData.size();
+ for (final PtNode ptNode : ptNodeArray.mData) {
+ ++ptNodes;
+ if (ptNode.mChars.length > maxRuns) maxRuns = ptNode.mChars.length;
+ if (ptNode.mFrequency >= 0) {
+ if (ptNodeArray.mCachedAddressAfterUpdate < firstTerminalAddress)
+ firstTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate;
+ if (ptNodeArray.mCachedAddressAfterUpdate > lastTerminalAddress)
+ lastTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate;
+ }
+ }
+ if (ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize > size) {
+ size = ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize;
+ }
+ }
+ final int[] ptNodeCounts = new int[maxNodes + 1];
+ final int[] runCounts = new int[maxRuns + 1];
+ for (final PtNodeArray ptNodeArray : ptNodeArrays) {
+ ++ptNodeCounts[ptNodeArray.mData.size()];
+ for (final PtNode ptNode : ptNodeArray.mData) {
+ ++runCounts[ptNode.mChars.length];
+ }
+ }
+
+ MakedictLog.i("Statistics:\n"
+ + " total file size " + size + "\n"
+ + " " + ptNodeArrays.size() + " node arrays\n"
+ + " " + ptNodes + " PtNodes (" + ((float)ptNodes / ptNodeArrays.size())
+ + " PtNodes per node)\n"
+ + " first terminal at " + firstTerminalAddress + "\n"
+ + " last terminal at " + lastTerminalAddress + "\n"
+ + " PtNode stats : max = " + maxNodes);
+ for (int i = 0; i < ptNodeCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + ptNodeCounts[i]);
+ }
+ MakedictLog.i(" Character run stats : max = " + maxRuns);
+ for (int i = 0; i < runCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + runCounts[i]);
+ }
+ }
+
+ /**
+ * Dumps a FusionDictionary to a file.
+ *
+ * @param destination the stream to write the binary data to.
+ * @param dict the dictionary to write.
+ * @param formatOptions file format options.
+ */
+ /* package */ static void writeDictionaryBinary(final OutputStream destination,
+ final FusionDictionary dict, final FormatOptions formatOptions)
+ throws IOException, UnsupportedFormatException {
+
+ // Addresses are limited to 3 bytes, but since addresses can be relative to each node
+ // array, the structure itself is not limited to 16MB. However, if it is over 16MB deciding
+ // the order of the PtNode arrays becomes a quite complicated problem, because though the
+ // dictionary itself does not have a size limit, each node array must still be within 16MB
+ // of all its children and parents. As long as this is ensured, the dictionary file may
+ // grow to any size.
+
+ final int version = formatOptions.mVersion;
+ if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
+ || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
+ throw new UnsupportedFormatException("Requested file format version " + version
+ + ", but this implementation only supports versions "
+ + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through "
+ + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
+
+ // The magic number in big-endian order.
+ // Magic number for all versions.
+ headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 24)));
+ headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 16)));
+ headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 8)));
+ headerBuffer.write((byte) (0xFF & FormatSpec.MAGIC_NUMBER));
+ // Dictionary version.
+ headerBuffer.write((byte) (0xFF & (version >> 8)));
+ headerBuffer.write((byte) (0xFF & version));
+
+ // Options flags
+ final int options = makeOptionsValue(dict, formatOptions);
+ headerBuffer.write((byte) (0xFF & (options >> 8)));
+ headerBuffer.write((byte) (0xFF & options));
+ final int headerSizeOffset = headerBuffer.size();
+ // Placeholder to be written later with header size.
+ for (int i = 0; i < 4; ++i) {
+ headerBuffer.write(0);
+ }
+ // Write out the options.
+ for (final String key : dict.mOptions.mAttributes.keySet()) {
+ final String value = dict.mOptions.mAttributes.get(key);
+ CharEncoding.writeString(headerBuffer, key);
+ CharEncoding.writeString(headerBuffer, value);
+ }
+ final int size = headerBuffer.size();
+ final byte[] bytes = headerBuffer.toByteArray();
+ // Write out the header size.
+ bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24));
+ bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16));
+ bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8));
+ bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0));
+ destination.write(bytes);
+
+ headerBuffer.close();
+
+ // Leave the choice of the optimal node order to the flattenTree function.
+ MakedictLog.i("Flattening the tree...");
+ ArrayList<PtNodeArray> flatNodes = flattenTree(dict.mRootNodeArray);
+
+ MakedictLog.i("Computing addresses...");
+ computeAddresses(dict, flatNodes, formatOptions);
+ MakedictLog.i("Checking PtNode array...");
+ if (DBG) checkFlatPtNodeArrayList(flatNodes);
+
+ // Create a buffer that matches the final dictionary size.
+ final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1);
+ final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize;
+ final byte[] buffer = new byte[bufferSize];
+ int index = 0;
+
+ MakedictLog.i("Writing file...");
+ int dataEndOffset = 0;
+ for (PtNodeArray nodeArray : flatNodes) {
+ dataEndOffset = writePlacedNode(dict, buffer, nodeArray, formatOptions);
+ }
+
+ if (DBG) showStatistics(flatNodes);
+
+ destination.write(buffer, 0, dataEndOffset);
+
+ destination.close();
+ MakedictLog.i("Done");
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index 167c6915c..106f02519 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -18,58 +18,55 @@ package com.android.inputmethod.latin.makedict;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.CharEncoding;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.ByteArrayDictBuffer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
-import java.nio.channels.FileChannel;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
public final class BinaryDictIOUtils {
private static final boolean DBG = false;
- private static final int MSB24 = 0x800000;
- private static final int SINT24_MAX = 0x7FFFFF;
- private static final int MAX_JUMPS = 10000;
private BinaryDictIOUtils() {
// This utility class is not publicly instantiable.
}
private static final class Position {
- public static final int NOT_READ_GROUPCOUNT = -1;
+ public static final int NOT_READ_PTNODE_COUNT = -1;
public int mAddress;
- public int mNumOfCharGroup;
+ public int mNumOfPtNode;
public int mPosition;
public int mLength;
public Position(int address, int length) {
mAddress = address;
mLength = length;
- mNumOfCharGroup = NOT_READ_GROUPCOUNT;
+ mNumOfPtNode = NOT_READ_PTNODE_COUNT;
}
}
/**
- * Tours all node without recursive call.
+ * Retrieves all node arrays without recursive call.
*/
private static void readUnigramsAndBigramsBinaryInner(
- final FusionDictionaryBufferInterface buffer, final int headerSize,
+ final Ver3DictDecoder dictDecoder, final int headerSize,
final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
final Map<Integer, ArrayList<PendingAttribute>> bigrams,
final FormatOptions formatOptions) {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1];
Stack<Position> stack = new Stack<Position>();
@@ -82,46 +79,45 @@ public final class BinaryDictIOUtils {
Position p = stack.peek();
if (DBG) {
- MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" +
- p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength);
+ MakedictLog.d("read: address=" + p.mAddress + ", numOfPtNode=" +
+ p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength);
}
- if (buffer.position() != p.mAddress) buffer.position(p.mAddress);
+ if (dictBuffer.position() != p.mAddress) dictBuffer.position(p.mAddress);
if (index != p.mLength) index = p.mLength;
- if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) {
- p.mNumOfCharGroup = BinaryDictInputOutput.readCharGroupCount(buffer);
- p.mAddress += BinaryDictInputOutput.getGroupCountSize(p.mNumOfCharGroup);
+ if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) {
+ p.mNumOfPtNode = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ p.mAddress += getPtNodeCountSize(p.mNumOfPtNode);
p.mPosition = 0;
}
- if (p.mNumOfCharGroup == 0) {
+ if (p.mNumOfPtNode == 0) {
stack.pop();
continue;
}
- CharGroupInfo info = BinaryDictInputOutput.readCharGroup(buffer,
- p.mAddress - headerSize, formatOptions);
+ PtNodeInfo info = dictDecoder.readPtNode(p.mAddress, formatOptions);
for (int i = 0; i < info.mCharacters.length; ++i) {
pushedChars[index++] = info.mCharacters[i];
}
p.mPosition++;
- final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(info.mFlags,
+ final boolean isMovedPtNode = isMovedPtNode(info.mFlags,
formatOptions);
- final boolean isDeletedGroup = BinaryDictInputOutput.isDeletedGroup(info.mFlags,
+ final boolean isDeletedPtNode = isDeletedPtNode(info.mFlags,
formatOptions);
- if (!isMovedGroup && !isDeletedGroup
- && info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) {// found word
+ if (!isMovedPtNode && !isDeletedPtNode
+ && info.mFrequency != FusionDictionary.PtNode.NOT_A_TERMINAL) {// found word
words.put(info.mOriginalAddress, new String(pushedChars, 0, index));
frequencies.put(info.mOriginalAddress, info.mFrequency);
if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams);
}
- if (p.mPosition == p.mNumOfCharGroup) {
+ if (p.mPosition == p.mNumOfPtNode) {
if (formatOptions.mSupportsDynamicUpdate) {
- final int forwardLinkAddress = buffer.readUnsignedInt24();
+ final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
if (forwardLinkAddress != FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- // the node has a forward link.
- p.mNumOfCharGroup = Position.NOT_READ_GROUPCOUNT;
+ // The node array has a forward link.
+ p.mNumOfPtNode = Position.NOT_READ_PTNODE_COUNT;
p.mAddress = forwardLinkAddress;
} else {
stack.pop();
@@ -130,12 +126,12 @@ public final class BinaryDictIOUtils {
stack.pop();
}
} else {
- // the node has more groups.
- p.mAddress = buffer.position();
+ // The Ptnode array has more PtNodes.
+ p.mAddress = dictBuffer.position();
}
- if (!isMovedGroup && BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) {
- Position childrenPos = new Position(info.mChildrenAddress + headerSize, index);
+ if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) {
+ final Position childrenPos = new Position(info.mChildrenAddress, index);
stack.push(childrenPos);
}
}
@@ -143,61 +139,60 @@ public final class BinaryDictIOUtils {
/**
* Reads unigrams and bigrams from the binary file.
- * Doesn't make the memory representation of the dictionary.
+ * Doesn't store a full memory representation of the dictionary.
*
- * @param buffer the buffer to read.
+ * @param dictDecoder the dict decoder.
* @param words the map to store the address as a key and the word as a value.
* @param frequencies the map to store the address as a key and the frequency as a value.
* @param bigrams the map to store the address as a key and the list of address as a value.
- * @throws IOException
- * @throws UnsupportedFormatException
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
*/
- public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer,
+ /* package */ static void readUnigramsAndBigramsBinary(final Ver3DictDecoder dictDecoder,
final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException,
UnsupportedFormatException {
// Read header
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
- readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams,
- header.mFormatOptions);
+ final FileHeader header = dictDecoder.readHeader();
+ readUnigramsAndBigramsBinaryInner(dictDecoder, header.mHeaderSize, words,
+ frequencies, bigrams, header.mFormatOptions);
}
/**
- * Gets the address of the last CharGroup of the exact matching word in the dictionary.
+ * Gets the address of the last PtNode of the exact matching word in the dictionary.
* If no match is found, returns NOT_VALID_WORD.
*
- * @param buffer the buffer to read.
+ * @param dictDecoder the dict decoder.
* @param word the word we search for.
* @return the address of the terminal node.
- * @throws IOException
- * @throws UnsupportedFormatException
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
*/
@UsedForTesting
- public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer,
+ /* package */ static int getTerminalPosition(final Ver3DictDecoder dictDecoder,
final String word) throws IOException, UnsupportedFormatException {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
if (word == null) return FormatSpec.NOT_VALID_WORD;
- if (buffer.position() != 0) buffer.position(0);
+ if (dictBuffer.position() != 0) dictBuffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+ final FileHeader header = dictDecoder.readHeader();
int wordPos = 0;
final int wordLen = word.codePointCount(0, word.length());
for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD;
do {
- final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
- boolean foundNextCharGroup = false;
- for (int i = 0; i < charGroupCount; ++i) {
- final int charGroupPos = buffer.position();
- final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
- buffer.position(), header.mFormatOptions);
- final boolean isMovedGroup =
- BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- final boolean isDeletedGroup =
- BinaryDictInputOutput.isDeletedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- if (isMovedGroup) continue;
+ final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ boolean foundNextPtNode = false;
+ for (int i = 0; i < ptNodeCount; ++i) {
+ final int ptNodePos = dictBuffer.position();
+ final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos,
+ header.mFormatOptions);
+ final boolean isMovedNode = isMovedPtNode(currentInfo.mFlags,
+ header.mFormatOptions);
+ final boolean isDeletedNode = isDeletedPtNode(currentInfo.mFlags,
+ header.mFormatOptions);
+ if (isMovedNode) continue;
boolean same = true;
for (int p = 0, j = word.offsetByCodePoints(0, wordPos);
p < currentInfo.mCharacters.length;
@@ -210,86 +205,60 @@ public final class BinaryDictIOUtils {
}
if (same) {
- // found the group matches the word.
+ // found the PtNode matches the word.
if (wordPos + currentInfo.mCharacters.length == wordLen) {
- if (currentInfo.mFrequency == CharGroup.NOT_A_TERMINAL
- || isDeletedGroup) {
+ if (currentInfo.mFrequency == PtNode.NOT_A_TERMINAL
+ || isDeletedNode) {
return FormatSpec.NOT_VALID_WORD;
} else {
- return charGroupPos;
+ return ptNodePos;
}
}
wordPos += currentInfo.mCharacters.length;
if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
return FormatSpec.NOT_VALID_WORD;
}
- foundNextCharGroup = true;
- buffer.position(currentInfo.mChildrenAddress);
+ foundNextPtNode = true;
+ dictBuffer.position(currentInfo.mChildrenAddress);
break;
}
}
- // If we found the next char group, it is under the file pointer.
- // But if not, we are at the end of this node so we expect to have
+ // If we found the next PtNode, it is under the file pointer.
+ // But if not, we are at the end of this node array so we expect to have
// a forward link address that we need to consult and possibly resume
- // search on the next node in the linked list.
- if (foundNextCharGroup) break;
+ // search on the next node array in the linked list.
+ if (foundNextPtNode) break;
if (!header.mFormatOptions.mSupportsDynamicUpdate) {
return FormatSpec.NOT_VALID_WORD;
}
- final int forwardLinkAddress = buffer.readUnsignedInt24();
+ final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
return FormatSpec.NOT_VALID_WORD;
}
- buffer.position(forwardLinkAddress);
+ dictBuffer.position(forwardLinkAddress);
} while(true);
}
return FormatSpec.NOT_VALID_WORD;
}
- private static int markAsDeleted(final int flags) {
- return (flags & (~FormatSpec.MASK_GROUP_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
- }
-
- /**
- * Delete the word from the binary file.
- *
- * @param buffer the buffer to write.
- * @param word the word we delete
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- @UsedForTesting
- public static void deleteWord(final FusionDictionaryBufferInterface buffer,
- final String word) throws IOException, UnsupportedFormatException {
- buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
- final int wordPosition = getTerminalPosition(buffer, word);
- if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
-
- buffer.position(wordPosition);
- final int flags = buffer.readUnsignedByte();
- buffer.position(wordPosition);
- buffer.put((byte)markAsDeleted(flags));
- }
-
/**
* @return the size written, in bytes. Always 3 bytes.
*/
- private static int writeSInt24ToBuffer(final FusionDictionaryBufferInterface buffer,
+ static int writeSInt24ToBuffer(final DictBuffer dictBuffer,
final int value) {
final int absValue = Math.abs(value);
- buffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
- buffer.put((byte)((absValue >> 8) & 0xFF));
- buffer.put((byte)(absValue & 0xFF));
+ dictBuffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
+ dictBuffer.put((byte)((absValue >> 8) & 0xFF));
+ dictBuffer.put((byte)(absValue & 0xFF));
return 3;
}
/**
* @return the size written, in bytes. Always 3 bytes.
*/
- private static int writeSInt24ToStream(final OutputStream destination, final int value)
+ static int writeSInt24ToStream(final OutputStream destination, final int value)
throws IOException {
final int absValue = Math.abs(value);
destination.write((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
@@ -303,7 +272,7 @@ public final class BinaryDictIOUtils {
*/
private static int writeVariableAddress(final OutputStream destination, final int value)
throws IOException {
- switch (BinaryDictInputOutput.getByteSize(value)) {
+ switch (BinaryDictEncoderUtils.getByteSize(value)) {
case 1:
destination.write((byte)value);
break;
@@ -317,111 +286,52 @@ public final class BinaryDictIOUtils {
destination.write((byte)(0xFF & value));
break;
}
- return BinaryDictInputOutput.getByteSize(value);
- }
-
- /**
- * Update a parent address in a CharGroup that is referred to by groupOriginAddress.
- *
- * @param buffer the buffer to write.
- * @param groupOriginAddress the address of the group.
- * @param newParentAddress the absolute address of the parent.
- * @param formatOptions file format options.
- */
- public static void updateParentAddress(final FusionDictionaryBufferInterface buffer,
- final int groupOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(groupOriginAddress);
- if (!formatOptions.mSupportsDynamicUpdate) {
- throw new RuntimeException("this file format does not support parent addresses");
- }
- final int flags = buffer.readUnsignedByte();
- if (BinaryDictInputOutput.isMovedGroup(flags, formatOptions)) {
- // if the group is moved, the parent address is stored in the destination group.
- // We are guaranteed to process the destination group later, so there is no need to
- // update anything here.
- buffer.position(originalPosition);
- return;
- }
- if (DBG) {
- MakedictLog.d("update parent address flags=" + flags + ", " + groupOriginAddress);
- }
- final int parentOffset = newParentAddress - groupOriginAddress;
- writeSInt24ToBuffer(buffer, parentOffset);
- buffer.position(originalPosition);
+ return BinaryDictEncoderUtils.getByteSize(value);
}
- private static void skipCharGroup(final FusionDictionaryBufferInterface buffer,
- final FormatOptions formatOptions) {
- final int flags = buffer.readUnsignedByte();
- BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
- skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
- BinaryDictInputOutput.readChildrenAddress(buffer, flags, formatOptions);
- if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
+ 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 = buffer.readUnsignedShort();
- buffer.position(buffer.position() + shortcutsSize
- - FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE);
+ 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_GROUP) {
- final int bigramFlags = buffer.readUnsignedByte();
- switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- buffer.readUnsignedByte();
+ 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_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- buffer.readUnsignedShort();
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
+ dictBuffer.readUnsignedShort();
break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- buffer.readUnsignedInt24();
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
+ dictBuffer.readUnsignedInt24();
break;
}
- if ((bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT) == 0) break;
+ if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break;
}
- if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- throw new RuntimeException("Too many bigrams in a group.");
+ if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ throw new RuntimeException("Too many bigrams in a PtNode.");
}
}
}
- /**
- * Update parent addresses in a Node that is referred to by nodeOriginAddress.
- *
- * @param buffer the buffer to be modified.
- * @param nodeOriginAddress the address of a modified Node.
- * @param newParentAddress the address to be written.
- * @param formatOptions file format options.
- */
- public static void updateParentAddresses(final FusionDictionaryBufferInterface buffer,
- final int nodeOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(nodeOriginAddress);
- do {
- final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
- for (int i = 0; i < count; ++i) {
- updateParentAddress(buffer, buffer.position(), newParentAddress, formatOptions);
- skipCharGroup(buffer, formatOptions);
- }
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- buffer.position(forwardLinkAddress);
- } while (formatOptions.mSupportsDynamicUpdate
- && buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
- buffer.position(originalPosition);
- }
-
- private static void skipString(final FusionDictionaryBufferInterface buffer,
+ static void skipString(final DictBuffer dictBuffer,
final boolean hasMultipleChars) {
if (hasMultipleChars) {
- int character = CharEncoding.readChar(buffer);
+ int character = CharEncoding.readChar(dictBuffer);
while (character != FormatSpec.INVALID_CHARACTER) {
- character = CharEncoding.readChar(buffer);
+ character = CharEncoding.readChar(dictBuffer);
}
} else {
- CharEncoding.readChar(buffer);
+ CharEncoding.readChar(dictBuffer);
}
}
@@ -449,46 +359,24 @@ public final class BinaryDictIOUtils {
size += 3;
}
}
- destination.write((byte)FormatSpec.GROUP_CHARACTERS_TERMINATOR);
- size += FormatSpec.GROUP_TERMINATOR_SIZE;
+ destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ size += FormatSpec.PTNODE_TERMINATOR_SIZE;
return size;
}
/**
- * Update a children address in a CharGroup that is addressed by groupOriginAddress.
- *
- * @param buffer the buffer to write.
- * @param groupOriginAddress the address of the group.
- * @param newChildrenAddress the absolute address of the child.
- * @param formatOptions file format options.
- */
- public static void updateChildrenAddress(final FusionDictionaryBufferInterface buffer,
- final int groupOriginAddress, final int newChildrenAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(groupOriginAddress);
- final int flags = buffer.readUnsignedByte();
- final int parentAddress = BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
- skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
- if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
- final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
- ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - buffer.position();
- writeSInt24ToBuffer(buffer, childrenOffset);
- buffer.position(originalPosition);
- }
-
- /**
- * Write a char group to an output stream.
- * A char group is an in-memory representation of a node in trie.
- * A char group info is an on-disk representation of a node.
+ * Write a PtNode to an output stream from a PtNodeInfo.
+ * A PtNode is an in-memory representation of a node in the patricia trie.
+ * A PtNode info is a container for low-level information about how the
+ * PtNode is stored in the binary format.
*
* @param destination the stream to write.
- * @param info the char group info to be written.
+ * @param info the PtNode info to be written.
* @return the size written, in bytes.
*/
- public static int writeCharGroup(final OutputStream destination, final CharGroupInfo info)
+ private static int writePtNode(final OutputStream destination, final PtNodeInfo info)
throws IOException {
- int size = FormatSpec.GROUP_FLAGS_SIZE;
+ int size = FormatSpec.PTNODE_FLAGS_SIZE;
destination.write((byte)info.mFlags);
final int parentOffset = info.mParentAddress == FormatSpec.NO_PARENT_ADDRESS ?
FormatSpec.NO_PARENT_ADDRESS : info.mParentAddress - info.mOriginalAddress;
@@ -503,7 +391,7 @@ public final class BinaryDictIOUtils {
}
}
if (info.mCharacters.length > 1) {
- destination.write((byte)FormatSpec.GROUP_CHARACTERS_TERMINATOR);
+ destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
size++;
}
@@ -513,7 +401,7 @@ public final class BinaryDictIOUtils {
}
if (DBG) {
- MakedictLog.d("writeCharGroup origin=" + info.mOriginalAddress + ", size=" + size
+ MakedictLog.d("writePtNode origin=" + info.mOriginalAddress + ", size=" + size
+ ", child=" + info.mChildrenAddress + ", characters ="
+ new String(info.mCharacters, 0, info.mCharacters.length));
}
@@ -524,14 +412,14 @@ public final class BinaryDictIOUtils {
if (info.mShortcutTargets != null && info.mShortcutTargets.size() > 0) {
final int shortcutListSize =
- BinaryDictInputOutput.getShortcutListSize(info.mShortcutTargets);
+ BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets);
destination.write((byte)(shortcutListSize >> 8));
destination.write((byte)(shortcutListSize & 0xFF));
size += 2;
final Iterator<WeightedString> shortcutIterator = info.mShortcutTargets.iterator();
while (shortcutIterator.hasNext()) {
final WeightedString target = shortcutIterator.next();
- destination.write((byte)BinaryDictInputOutput.makeShortcutFlags(
+ destination.write((byte)BinaryDictEncoderUtils.makeShortcutFlags(
shortcutIterator.hasNext(), target.mFrequency));
size++;
size += writeString(destination, target.mWord);
@@ -540,28 +428,28 @@ public final class BinaryDictIOUtils {
if (info.mBigrams != null) {
// TODO: Consolidate this code with the code that computes the size of the bigram list
- // in BinaryDictionaryInputOutput#computeActualNodeSize
+ // in BinaryDictEncoderUtils#computeActualNodeArraySize
for (int i = 0; i < info.mBigrams.size(); ++i) {
final int bigramFrequency = info.mBigrams.get(i).mFrequency;
int bigramFlags = (i < info.mBigrams.size() - 1)
- ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0;
+ ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0;
size++;
final int bigramOffset = info.mBigrams.get(i).mAddress - (info.mOriginalAddress
+ size);
- bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0;
- switch (BinaryDictInputOutput.getByteSize(bigramOffset)) {
+ bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0;
+ switch (BinaryDictEncoderUtils.getByteSize(bigramOffset)) {
case 1:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE;
break;
case 2:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES;
break;
case 3:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES;
break;
}
- bigramFlags |= bigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY;
+ bigramFlags |= bigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
destination.write((byte)bigramFlags);
size += writeVariableAddress(destination, Math.abs(bigramOffset));
}
@@ -569,82 +457,40 @@ public final class BinaryDictIOUtils {
return size;
}
- @SuppressWarnings("unused")
- private static void updateForwardLink(final FusionDictionaryBufferInterface buffer,
- final int nodeOriginAddress, final int newNodeAddress,
- final FormatOptions formatOptions) {
- buffer.position(nodeOriginAddress);
- int jumpCount = 0;
- while (jumpCount++ < MAX_JUMPS) {
- final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
- for (int i = 0; i < count; ++i) skipCharGroup(buffer, formatOptions);
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- buffer.position(buffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
- writeSInt24ToBuffer(buffer, newNodeAddress);
- return;
- }
- buffer.position(forwardLinkAddress);
- }
- if (DBG && jumpCount >= MAX_JUMPS) {
- throw new RuntimeException("too many jumps, probably a bug.");
- }
- }
-
/**
- * Helper method to move a char group to the tail of the file.
+ * Compute the size of the PtNode.
*/
- private static int moveCharGroup(final OutputStream destination,
- final FusionDictionaryBufferInterface buffer, final CharGroupInfo info,
- final int nodeOriginAddress, final int oldGroupAddress,
- final FormatOptions formatOptions) throws IOException {
- updateParentAddress(buffer, oldGroupAddress, buffer.limit() + 1, formatOptions);
- buffer.position(oldGroupAddress);
- final int currentFlags = buffer.readUnsignedByte();
- buffer.position(oldGroupAddress);
- buffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
- & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
- int size = FormatSpec.GROUP_FLAGS_SIZE;
- updateForwardLink(buffer, nodeOriginAddress, buffer.limit(), formatOptions);
- size += writeNode(destination, new CharGroupInfo[] { info });
- return size;
- }
-
- /**
- * Compute the size of the char group.
- */
- private static int computeGroupSize(final CharGroupInfo info,
- final FormatOptions formatOptions) {
- int size = FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
- + BinaryDictInputOutput.getGroupCharactersSize(info.mCharacters)
- + BinaryDictInputOutput.getChildrenAddressSize(info.mFlags, formatOptions);
+ static int computePtNodeSize(final PtNodeInfo info, final FormatOptions formatOptions) {
+ int size = FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
+ + BinaryDictEncoderUtils.getPtNodeCharactersSize(info.mCharacters)
+ + getChildrenAddressSize(info.mFlags, formatOptions);
if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) {
- size += FormatSpec.GROUP_FREQUENCY_SIZE;
+ size += FormatSpec.PTNODE_FREQUENCY_SIZE;
}
if (info.mShortcutTargets != null && !info.mShortcutTargets.isEmpty()) {
- size += BinaryDictInputOutput.getShortcutListSize(info.mShortcutTargets);
+ size += BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets);
}
if (info.mBigrams != null) {
for (final PendingAttribute attr : info.mBigrams) {
- size += FormatSpec.GROUP_FLAGS_SIZE;
- size += BinaryDictInputOutput.getByteSize(attr.mAddress);
+ size += FormatSpec.PTNODE_FLAGS_SIZE;
+ size += BinaryDictEncoderUtils.getByteSize(attr.mAddress);
}
}
return size;
}
/**
- * Write a node to the stream.
+ * Write a node array to the stream.
*
* @param destination the stream to write.
- * @param infos groups to be written.
+ * @param infos an array of PtNodeInfo to be written.
* @return the size written, in bytes.
* @throws IOException
*/
- private static int writeNode(final OutputStream destination, final CharGroupInfo[] infos)
+ static int writeNodes(final OutputStream destination, final PtNodeInfo[] infos)
throws IOException {
- int size = BinaryDictInputOutput.getGroupCountSize(infos.length);
- switch (BinaryDictInputOutput.getGroupCountSize(infos.length)) {
+ int size = getPtNodeCountSize(infos.length);
+ switch (getPtNodeCountSize(infos.length)) {
case 1:
destination.write((byte)infos.length);
break;
@@ -653,335 +499,13 @@ public final class BinaryDictIOUtils {
destination.write((byte)(infos.length & 0xFF));
break;
default:
- throw new RuntimeException("Invalid group count size.");
+ throw new RuntimeException("Invalid node count size.");
}
- for (final CharGroupInfo info : infos) size += writeCharGroup(destination, info);
+ for (final PtNodeInfo info : infos) size += writePtNode(destination, info);
writeSInt24ToStream(destination, FormatSpec.NO_FORWARD_LINK_ADDRESS);
return size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
}
- /**
- * Move a group that is referred to by oldGroupOrigin to the tail of the file.
- * And set the children address to the byte after the group.
- *
- * @param nodeOrigin the address of the tail of the file.
- * @param characters
- * @param length
- * @param flags
- * @param frequency
- * @param parentAddress
- * @param shortcutTargets
- * @param bigrams
- * @param destination the stream representing the tail of the file.
- * @param buffer the buffer representing the (constant-size) body of the file.
- * @param oldNodeOrigin
- * @param oldGroupOrigin
- * @param formatOptions
- * @return the size written, in bytes.
- * @throws IOException
- */
- private static int moveGroup(final int nodeOrigin, final int[] characters, final int length,
- final int flags, final int frequency, final int parentAddress,
- final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
- final FusionDictionaryBufferInterface buffer, final int oldNodeOrigin,
- final int oldGroupOrigin, final FormatOptions formatOptions) throws IOException {
- int size = 0;
- final int newGroupOrigin = nodeOrigin + 1;
- final int[] writtenCharacters = Arrays.copyOfRange(characters, 0, length);
- final CharGroupInfo tmpInfo = new CharGroupInfo(newGroupOrigin, -1 /* endAddress */,
- flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcutTargets, bigrams);
- size = computeGroupSize(tmpInfo, formatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newGroupOrigin, newGroupOrigin + size,
- flags, writtenCharacters, frequency, parentAddress,
- nodeOrigin + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
- bigrams);
- moveCharGroup(destination, buffer, newInfo, oldNodeOrigin, oldGroupOrigin, formatOptions);
- return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
-
- /**
- * Insert a word into a binary dictionary.
- *
- * @param buffer
- * @param destination
- * @param word
- * @param frequency
- * @param bigramStrings
- * @param shortcuts
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- // TODO: Support batch insertion.
- // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
- @UsedForTesting
- public static void insertWord(final FusionDictionaryBufferInterface buffer,
- final OutputStream destination, final String word, final int frequency,
- final ArrayList<WeightedString> bigramStrings,
- final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
- final boolean isBlackListEntry)
- throws IOException, UnsupportedFormatException {
- final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
- if (bigramStrings != null) {
- for (final WeightedString bigram : bigramStrings) {
- int position = getTerminalPosition(buffer, bigram.mWord);
- if (position == FormatSpec.NOT_VALID_WORD) {
- // TODO: figure out what is the correct thing to do here.
- } else {
- bigrams.add(new PendingAttribute(bigram.mFrequency, position));
- }
- }
- }
-
- final boolean isTerminal = true;
- final boolean hasBigrams = !bigrams.isEmpty();
- final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
-
- // find the insert position of the word.
- if (buffer.position() != 0) buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
-
- int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.position();
- final int[] codePoints = FusionDictionary.getCodePoints(word);
- final int wordLen = codePoints.length;
-
- for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
- if (wordPos >= wordLen) break;
- nodeOriginAddress = buffer.position();
- int nodeParentAddress = -1;
- final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
- boolean foundNextGroup = false;
-
- for (int i = 0; i < charGroupCount; ++i) {
- address = buffer.position();
- final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
- buffer.position(), header.mFormatOptions);
- final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- if (isMovedGroup) continue;
- nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
- ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
- boolean matched = true;
- for (int p = 0; p < currentInfo.mCharacters.length; ++p) {
- if (wordPos + p >= wordLen) {
- /*
- * splitting
- * before
- * abcd - ef
- *
- * insert "abc"
- *
- * after
- * abc - d - ef
- */
- final int newNodeAddress = buffer.limit();
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
- isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
- false /* isBlackListEntry */, header.mFormatOptions);
- int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags,
- frequency, nodeParentAddress, shortcuts, bigrams, destination,
- buffer, nodeOriginAddress, address, header.mFormatOptions);
-
- final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
- currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(buffer, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, header.mFormatOptions);
- }
- final CharGroupInfo newInfo2 = new CharGroupInfo(
- newNodeAddress + written + 1, -1 /* endAddress */,
- currentInfo.mFlags, characters2, currentInfo.mFrequency,
- newNodeAddress + 1, currentInfo.mChildrenAddress,
- currentInfo.mShortcutTargets, currentInfo.mBigrams);
- writeNode(destination, new CharGroupInfo[] { newInfo2 });
- return;
- } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) {
- if (p > 0) {
- /*
- * splitting
- * before
- * ab - cd
- *
- * insert "ac"
- *
- * after
- * a - b - cd
- * |
- * - c
- */
-
- final int newNodeAddress = buffer.limit();
- final int childrenAddress = currentInfo.mChildrenAddress;
-
- // move prefix
- final int prefixFlags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
- false /* isTerminal */, 0 /* childrenAddressSize*/,
- false /* hasShortcut */, false /* hasBigrams */,
- false /* isNotAWord */, false /* isBlackListEntry */,
- header.mFormatOptions);
- int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p,
- prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
- destination, buffer, nodeOriginAddress, address,
- header.mFormatOptions);
-
- final int[] suffixCharacters = Arrays.copyOfRange(
- currentInfo.mCharacters, p, currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(buffer, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, header.mFormatOptions);
- }
- final int suffixFlags = BinaryDictInputOutput.makeCharGroupFlags(
- suffixCharacters.length > 1,
- (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0,
- 0 /* childrenAddressSize */,
- (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
- != 0,
- (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo suffixInfo = new CharGroupInfo(
- newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
- suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
- currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
- currentInfo.mBigrams);
- written += computeGroupSize(suffixInfo, header.mFormatOptions) + 1;
-
- final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
- codePoints.length);
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(
- newCharacters.length > 1, isTerminal,
- 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(
- newNodeAddress + written, -1 /* endAddress */, flags,
- newCharacters, frequency, newNodeAddress + 1,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[] { suffixInfo, newInfo });
- return;
- }
- matched = false;
- break;
- }
- }
-
- if (matched) {
- if (wordPos + currentInfo.mCharacters.length == wordLen) {
- // the word exists in the dictionary.
- // only update group.
- final int newNodeAddress = buffer.limit();
- final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
- nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
- bigrams);
- moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address,
- header.mFormatOptions);
- return;
- }
- wordPos += currentInfo.mCharacters.length;
- if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
- /*
- * found the prefix of the word.
- * make new node and link to the node from this group.
- *
- * before
- * ab - cd
- *
- * insert "abcde"
- *
- * after
- * ab - cd - e
- */
- final int newNodeAddress = buffer.limit();
- updateChildrenAddress(buffer, address, newNodeAddress,
- header.mFormatOptions);
- final int newGroupAddress = newNodeAddress + 1;
- final boolean hasMultipleChars = (wordLen - wordPos) > 1;
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags,
- characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[] { newInfo });
- return;
- }
- buffer.position(currentInfo.mChildrenAddress);
- foundNextGroup = true;
- break;
- }
- }
-
- if (foundNextGroup) continue;
-
- // reached the end of the array.
- final int linkAddressPosition = buffer.position();
- int nextLink = buffer.readUnsignedInt24();
- if ((nextLink & MSB24) != 0) {
- nextLink = -(nextLink & SINT24_MAX);
- }
- if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- /*
- * expand this node.
- *
- * before
- * ab - cd
- *
- * insert "abef"
- *
- * after
- * ab - cd
- * |
- * - ef
- */
-
- // change the forward link address.
- final int newNodeAddress = buffer.limit();
- buffer.position(linkAddressPosition);
- writeSInt24ToBuffer(buffer, newNodeAddress);
-
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(characters.length > 1,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[]{ newInfo });
- return;
- } else {
- depth--;
- buffer.position(nextLink);
- }
- }
- }
-
- /**
- * Find a word from the buffer.
- *
- * @param buffer the buffer representing the body of the dictionary file.
- * @param word the word searched
- * @return the found group
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- @UsedForTesting
- public static CharGroupInfo findWordFromBuffer(final FusionDictionaryBufferInterface buffer,
- final String word) throws IOException, UnsupportedFormatException {
- int position = getTerminalPosition(buffer, word);
- if (position != FormatSpec.NOT_VALID_WORD) {
- buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
- buffer.position(position);
- return BinaryDictInputOutput.readCharGroup(buffer, position, header.mFormatOptions);
- }
- return null;
- }
-
private static final int HEADER_READING_BUFFER_SIZE = 16384;
/**
* Convenience method to read the header of a binary file.
@@ -992,20 +516,27 @@ public final class BinaryDictIOUtils {
* @param offset The offset in the file where to start reading the data.
* @param length The length of the data file.
*/
- public static FileHeader getDictionaryFileHeader(
+ private static FileHeader getDictionaryFileHeader(
final File file, final long offset, final long length)
throws FileNotFoundException, IOException, UnsupportedFormatException {
final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE];
- final FileInputStream inStream = new FileInputStream(file);
- try {
- inStream.read(buffer);
- final BinaryDictInputOutput.ByteBufferWrapper wrapper =
- new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, offset, length));
- return BinaryDictInputOutput.readHeader(wrapper);
- } finally {
- inStream.close();
- }
+ final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file,
+ new DictDecoder.DictionaryBufferFactory() {
+ @Override
+ public DictBuffer getDictionaryBuffer(File file)
+ throws FileNotFoundException, IOException {
+ final FileInputStream inStream = new FileInputStream(file);
+ try {
+ inStream.skip(offset);
+ inStream.read(buffer);
+ return new ByteArrayDictBuffer(buffer);
+ } finally {
+ inStream.close();
+ }
+ }
+ }
+ );
+ return dictDecoder.readHeader();
}
public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset,
@@ -1019,4 +550,83 @@ public final class BinaryDictIOUtils {
return null;
}
}
+
+ /**
+ * Helper method to hide the actual value of the no children address.
+ */
+ public static boolean hasChildrenAddress(final int address) {
+ return FormatSpec.NO_CHILDREN_ADDRESS != address;
+ }
+
+ /**
+ * Helper method to check whether the node is moved.
+ */
+ public static boolean isMovedPtNode(final int flags, final FormatOptions options) {
+ return options.mSupportsDynamicUpdate
+ && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
+ }
+
+ /**
+ * Helper method to check whether the dictionary can be updated dynamically.
+ */
+ public static boolean supportsDynamicUpdate(final FormatOptions options) {
+ return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE
+ && options.mSupportsDynamicUpdate;
+ }
+
+ /**
+ * Helper method to check whether the node is deleted.
+ */
+ public static boolean isDeletedPtNode(final int flags, final FormatOptions formatOptions) {
+ return formatOptions.mSupportsDynamicUpdate
+ && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
+ }
+
+ /**
+ * Compute the binary size of the node count
+ * @param count the node count
+ * @return the size of the node count, either 1 or 2 bytes.
+ */
+ public static int getPtNodeCountSize(final int count) {
+ if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= count) {
+ return 1;
+ } else if (FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY >= count) {
+ return 2;
+ } else {
+ throw new RuntimeException("Can't have more than "
+ + FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY + " PtNode in a PtNodeArray (found "
+ + count + ")");
+ }
+ }
+
+ static int getChildrenAddressSize(final int optionFlags,
+ final FormatOptions formatOptions) {
+ if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
+ switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
+ return 1;
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES:
+ return 2;
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES:
+ return 3;
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS:
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Calculate bigram frequency from compressed value
+ *
+ * @param unigramFrequency
+ * @param bigramFrequency compressed frequency
+ * @return approximate bigram frequency
+ */
+ public static int reconstructBigramFrequency(final int unigramFrequency,
+ final int bigramFrequency) {
+ final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
+ / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY);
+ final float resultFreqFloat = unigramFrequency + stepSize * (bigramFrequency + 1.0f);
+ return (int)resultFreqFloat;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
deleted file mode 100644
index 1b187d85d..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ /dev/null
@@ -1,1838 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin.makedict;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
-import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * Reads and writes XML files for a FusionDictionary.
- *
- * All the methods in this class are static.
- */
-public final class BinaryDictInputOutput {
-
- private static final boolean DBG = MakedictLog.DBG;
-
- // Arbitrary limit to how much passes we consider address size compression should
- // terminate in. At the time of this writing, our largest dictionary completes
- // compression in five passes.
- // If the number of passes exceeds this number, makedict bails with an exception on
- // suspicion that a bug might be causing an infinite loop.
- private static final int MAX_PASSES = 24;
- private static final int MAX_JUMPS = 12;
-
- @UsedForTesting
- public interface FusionDictionaryBufferInterface {
- public int readUnsignedByte();
- public int readUnsignedShort();
- public int readUnsignedInt24();
- public int readInt();
- public int position();
- public void position(int newPosition);
- public void put(final byte b);
- public int limit();
- public int capacity();
- }
-
- public static final class ByteBufferWrapper implements FusionDictionaryBufferInterface {
- private ByteBuffer mBuffer;
-
- public ByteBufferWrapper(final ByteBuffer buffer) {
- mBuffer = buffer;
- }
-
- @Override
- public int readUnsignedByte() {
- return mBuffer.get() & 0xFF;
- }
-
- @Override
- public int readUnsignedShort() {
- return mBuffer.getShort() & 0xFFFF;
- }
-
- @Override
- public int readUnsignedInt24() {
- final int retval = readUnsignedByte();
- return (retval << 16) + readUnsignedShort();
- }
-
- @Override
- public int readInt() {
- return mBuffer.getInt();
- }
-
- @Override
- public int position() {
- return mBuffer.position();
- }
-
- @Override
- public void position(int newPos) {
- mBuffer.position(newPos);
- }
-
- @Override
- public void put(final byte b) {
- mBuffer.put(b);
- }
-
- @Override
- public int limit() {
- return mBuffer.limit();
- }
-
- @Override
- public int capacity() {
- return mBuffer.capacity();
- }
- }
-
- /**
- * A class grouping utility function for our specific character encoding.
- */
- static final class CharEncoding {
- private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
- private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
-
- /**
- * Helper method to find out whether this code fits on one byte
- */
- private static boolean fitsOnOneByte(final int character) {
- return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
- && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
- }
-
- /**
- * Compute the size of a character given its character code.
- *
- * Char format is:
- * 1 byte = bbbbbbbb match
- * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
- * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
- * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
- * 00011111 would be outside unicode.
- * else: iso-latin-1 code
- * This allows for the whole unicode range to be encoded, including chars outside of
- * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
- * characters which should never happen anyway (and still work, but take 3 bytes).
- *
- * @param character the character code.
- * @return the size in binary encoded-form, either 1 or 3 bytes.
- */
- static int getCharSize(final int character) {
- // See char encoding in FusionDictionary.java
- if (fitsOnOneByte(character)) return 1;
- if (FormatSpec.INVALID_CHARACTER == character) return 1;
- return 3;
- }
-
- /**
- * Compute the byte size of a character array.
- */
- private static int getCharArraySize(final int[] chars) {
- int size = 0;
- for (int character : chars) size += getCharSize(character);
- return size;
- }
-
- /**
- * Writes a char array to a byte buffer.
- *
- * @param codePoints the code point array to write.
- * @param buffer the byte buffer to write to.
- * @param index the index in buffer to write the character array to.
- * @return the index after the last character.
- */
- private static int writeCharArray(final int[] codePoints, final byte[] buffer, int index) {
- for (int codePoint : codePoints) {
- if (1 == getCharSize(codePoint)) {
- buffer[index++] = (byte)codePoint;
- } else {
- buffer[index++] = (byte)(0xFF & (codePoint >> 16));
- buffer[index++] = (byte)(0xFF & (codePoint >> 8));
- buffer[index++] = (byte)(0xFF & codePoint);
- }
- }
- return index;
- }
-
- /**
- * Writes a string with our character format to a byte buffer.
- *
- * This will also write the terminator byte.
- *
- * @param buffer the byte buffer to write to.
- * @param origin the offset to write from.
- * @param word the string to write.
- * @return the size written, in bytes.
- */
- private static int writeString(final byte[] buffer, final int origin,
- final String word) {
- final int length = word.length();
- int index = origin;
- for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
- final int codePoint = word.codePointAt(i);
- if (1 == getCharSize(codePoint)) {
- buffer[index++] = (byte)codePoint;
- } else {
- buffer[index++] = (byte)(0xFF & (codePoint >> 16));
- buffer[index++] = (byte)(0xFF & (codePoint >> 8));
- buffer[index++] = (byte)(0xFF & codePoint);
- }
- }
- buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
- return index - origin;
- }
-
- /**
- * Writes a string with our character format to a ByteArrayOutputStream.
- *
- * This will also write the terminator byte.
- *
- * @param buffer the ByteArrayOutputStream to write to.
- * @param word the string to write.
- */
- private static void writeString(final ByteArrayOutputStream buffer, final String word) {
- final int length = word.length();
- for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
- final int codePoint = word.codePointAt(i);
- if (1 == getCharSize(codePoint)) {
- buffer.write((byte) codePoint);
- } else {
- buffer.write((byte) (0xFF & (codePoint >> 16)));
- buffer.write((byte) (0xFF & (codePoint >> 8)));
- buffer.write((byte) (0xFF & codePoint));
- }
- }
- buffer.write(FormatSpec.GROUP_CHARACTERS_TERMINATOR);
- }
-
- /**
- * Reads a string from a buffer. This is the converse of the above method.
- */
- private static String readString(final FusionDictionaryBufferInterface buffer) {
- final StringBuilder s = new StringBuilder();
- int character = readChar(buffer);
- while (character != FormatSpec.INVALID_CHARACTER) {
- s.appendCodePoint(character);
- character = readChar(buffer);
- }
- return s.toString();
- }
-
- /**
- * Reads a character from the buffer.
- *
- * This follows the character format documented earlier in this source file.
- *
- * @param buffer the buffer, positioned over an encoded character.
- * @return the character code.
- */
- static int readChar(final FusionDictionaryBufferInterface buffer) {
- int character = buffer.readUnsignedByte();
- if (!fitsOnOneByte(character)) {
- if (FormatSpec.GROUP_CHARACTERS_TERMINATOR == character) {
- return FormatSpec.INVALID_CHARACTER;
- }
- character <<= 16;
- character += buffer.readUnsignedShort();
- }
- return character;
- }
- }
-
- /**
- * Compute the binary size of the character array.
- *
- * If only one character, this is the size of this character. If many, it's the sum of their
- * sizes + 1 byte for the terminator.
- *
- * @param characters the character array
- * @return the size of the char array, including the terminator if any
- */
- static int getGroupCharactersSize(final int[] characters) {
- int size = CharEncoding.getCharArraySize(characters);
- if (characters.length > 1) size += FormatSpec.GROUP_TERMINATOR_SIZE;
- return size;
- }
-
- /**
- * Compute the binary size of the character array in a group
- *
- * If only one character, this is the size of this character. If many, it's the sum of their
- * sizes + 1 byte for the terminator.
- *
- * @param group the group
- * @return the size of the char array, including the terminator if any
- */
- private static int getGroupCharactersSize(final CharGroup group) {
- return getGroupCharactersSize(group.mChars);
- }
-
- /**
- * Compute the binary size of the group count
- * @param count the group count
- * @return the size of the group count, either 1 or 2 bytes.
- */
- public static int getGroupCountSize(final int count) {
- if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
- return 1;
- } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
- return 2;
- } else {
- throw new RuntimeException("Can't have more than "
- + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
- + ")");
- }
- }
-
- /**
- * Compute the binary size of the group count for a node
- * @param node the node
- * @return the size of the group count, either 1 or 2 bytes.
- */
- private static int getGroupCountSize(final Node node) {
- return getGroupCountSize(node.mData.size());
- }
-
- /**
- * Compute the size of a shortcut in bytes.
- */
- private static int getShortcutSize(final WeightedString shortcut) {
- int size = FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE;
- final String word = shortcut.mWord;
- final int length = word.length();
- for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
- final int codePoint = word.codePointAt(i);
- size += CharEncoding.getCharSize(codePoint);
- }
- size += FormatSpec.GROUP_TERMINATOR_SIZE;
- return size;
- }
-
- /**
- * Compute the size of a shortcut list in bytes.
- *
- * This is known in advance and does not change according to position in the file
- * like address lists do.
- */
- static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) {
- if (null == shortcutList) return 0;
- int size = FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- for (final WeightedString shortcut : shortcutList) {
- size += getShortcutSize(shortcut);
- }
- return size;
- }
-
- /**
- * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
- *
- * @param group the CharGroup to compute the size of.
- * @param options file format options.
- * @return the maximum size of the group.
- */
- private static int getCharGroupMaximumSize(final CharGroup group, final FormatOptions options) {
- int size = getGroupHeaderSize(group, options);
- // If terminal, one byte for the frequency
- if (group.isTerminal()) size += FormatSpec.GROUP_FREQUENCY_SIZE;
- size += FormatSpec.GROUP_MAX_ADDRESS_SIZE; // For children address
- size += getShortcutListSize(group.mShortcutTargets);
- if (null != group.mBigrams) {
- size += (FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE
- + FormatSpec.GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
- * group.mBigrams.size();
- }
- return size;
- }
-
- /**
- * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
- * it in the 'actualSize' member of the node.
- *
- * @param node the node to compute the maximum size of.
- * @param options file format options.
- */
- private static void calculateNodeMaximumSize(final Node node, final FormatOptions options) {
- int size = getGroupCountSize(node);
- for (CharGroup g : node.mData) {
- final int groupSize = getCharGroupMaximumSize(g, options);
- g.mCachedSize = groupSize;
- size += groupSize;
- }
- if (options.mSupportsDynamicUpdate) {
- size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- node.mCachedSize = size;
- }
-
- /**
- * Helper method to hide the actual value of the no children address.
- */
- public static boolean hasChildrenAddress(final int address) {
- return FormatSpec.NO_CHILDREN_ADDRESS != address;
- }
-
- /**
- * Helper method to check whether the group is moved.
- */
- public static boolean isMovedGroup(final int flags, final FormatOptions options) {
- return options.mSupportsDynamicUpdate
- && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
- }
-
- /**
- * Helper method to check whether the group is deleted.
- */
- public static boolean isDeletedGroup(final int flags, final FormatOptions formatOptions) {
- return formatOptions.mSupportsDynamicUpdate
- && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
- }
-
- /**
- * Helper method to check whether the dictionary can be updated dynamically.
- */
- public static boolean supportsDynamicUpdate(final FormatOptions options) {
- return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE
- && options.mSupportsDynamicUpdate;
- }
-
- /**
- * Compute the size of the header (flag + [parent address] + characters size) of a CharGroup.
- *
- * @param group the group of which to compute the size of the header
- * @param options file format options.
- */
- private static int getGroupHeaderSize(final CharGroup group, final FormatOptions options) {
- if (supportsDynamicUpdate(options)) {
- return FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
- + getGroupCharactersSize(group);
- } else {
- return FormatSpec.GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
- }
- }
-
- private static final int UINT8_MAX = 0xFF;
- private static final int UINT16_MAX = 0xFFFF;
- private static final int UINT24_MAX = 0xFFFFFF;
-
- /**
- * Compute the size, in bytes, that an address will occupy.
- *
- * This can be used either for children addresses (which are always positive) or for
- * attribute, which may be positive or negative but
- * store their sign bit separately.
- *
- * @param address the address
- * @return the byte size.
- */
- static int getByteSize(final int address) {
- assert(address <= UINT24_MAX);
- if (!hasChildrenAddress(address)) {
- return 0;
- } else if (Math.abs(address) <= UINT8_MAX) {
- return 1;
- } else if (Math.abs(address) <= UINT16_MAX) {
- return 2;
- } else {
- return 3;
- }
- }
-
- private static final int SINT24_MAX = 0x7FFFFF;
- private static final int MSB8 = 0x80;
- private static final int MSB24 = 0x800000;
-
- // End utility methods.
-
- // This method is responsible for finding a nice ordering of the nodes that favors run-time
- // cache performance and dictionary size.
- /* package for tests */ static ArrayList<Node> flattenTree(final Node root) {
- final int treeSize = FusionDictionary.countCharGroups(root);
- MakedictLog.i("Counted nodes : " + treeSize);
- final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
- return flattenTreeInner(flatTree, root);
- }
-
- private static ArrayList<Node> flattenTreeInner(final ArrayList<Node> list, final Node node) {
- // Removing the node is necessary if the tails are merged, because we would then
- // add the same node several times when we only want it once. A number of places in
- // the code also depends on any node being only once in the list.
- // Merging tails can only be done if there are no attributes. Searching for attributes
- // in LatinIME code depends on a total breadth-first ordering, which merging tails
- // breaks. If there are no attributes, it should be fine (and reduce the file size)
- // to merge tails, and removing the node from the list would be necessary. However,
- // we don't merge tails because breaking the breadth-first ordering would result in
- // extreme overhead at bigram lookup time (it would make the search function O(n) instead
- // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty
- // high).
- // If no nodes are ever merged, we can't have the same node twice in the list, hence
- // searching for duplicates in unnecessary. It is also very performance consuming,
- // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making
- // this simple list.remove operation O(n*n) overall. On Android this overhead is very
- // high.
- // For future reference, the code to remove duplicate is a simple : list.remove(node);
- list.add(node);
- final ArrayList<CharGroup> branches = node.mData;
- final int nodeSize = branches.size();
- for (CharGroup group : branches) {
- if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
- }
- return list;
- }
-
- /**
- * Get the offset from a position inside a current node to a target node, during update.
- *
- * If the current node is before the target node, the target node has not been updated yet,
- * so we should return the offset from the old position of the current node to the old position
- * of the target node. If on the other hand the target is before the current node, it already
- * has been updated, so we should return the offset from the new position in the current node
- * to the new position in the target node.
- * @param currentNode the node containing the CharGroup where the offset will be written
- * @param offsetFromStartOfCurrentNode the offset, in bytes, from the start of currentNode
- * @param targetNode the target node to get the offset to
- * @return the offset to the target node
- */
- private static int getOffsetToTargetNodeDuringUpdate(final Node currentNode,
- final int offsetFromStartOfCurrentNode, final Node targetNode) {
- final boolean isTargetBeforeCurrent = (targetNode.mCachedAddressBeforeUpdate
- < currentNode.mCachedAddressBeforeUpdate);
- if (isTargetBeforeCurrent) {
- return targetNode.mCachedAddressAfterUpdate
- - (currentNode.mCachedAddressAfterUpdate + offsetFromStartOfCurrentNode);
- } else {
- return targetNode.mCachedAddressBeforeUpdate
- - (currentNode.mCachedAddressBeforeUpdate + offsetFromStartOfCurrentNode);
- }
- }
-
- /**
- * Get the offset from a position inside a current node to a target CharGroup, during update.
- * @param currentNode the node containing the CharGroup where the offset will be written
- * @param offsetFromStartOfCurrentNode the offset, in bytes, from the start of currentNode
- * @param targetCharGroup the target CharGroup to get the offset to
- * @return the offset to the target CharGroup
- */
- // TODO: is there any way to factorize this method with the one above?
- private static int getOffsetToTargetCharGroupDuringUpdate(final Node currentNode,
- final int offsetFromStartOfCurrentNode, final CharGroup targetCharGroup) {
- final int oldOffsetBasePoint = currentNode.mCachedAddressBeforeUpdate
- + offsetFromStartOfCurrentNode;
- final boolean isTargetBeforeCurrent = (targetCharGroup.mCachedAddressBeforeUpdate
- < oldOffsetBasePoint);
- // If the target is before the current node, then its address has already been updated.
- // We can use the AfterUpdate member, and compare it to our own member after update.
- // Otherwise, the AfterUpdate member is not updated yet, so we need to use the BeforeUpdate
- // member, and of course we have to compare this to our own address before update.
- if (isTargetBeforeCurrent) {
- final int newOffsetBasePoint = currentNode.mCachedAddressAfterUpdate
- + offsetFromStartOfCurrentNode;
- return targetCharGroup.mCachedAddressAfterUpdate - newOffsetBasePoint;
- } else {
- return targetCharGroup.mCachedAddressBeforeUpdate - oldOffsetBasePoint;
- }
- }
-
- /**
- * Computes the actual node size, based on the cached addresses of the children nodes.
- *
- * Each node stores its tentative address. During dictionary address computing, these
- * are not final, but they can be used to compute the node size (the node size depends
- * on the address of the children because the number of bytes necessary to store an
- * address depends on its numeric value. The return value indicates whether the node
- * contents (as in, any of the addresses stored in the cache fields) have changed with
- * respect to their previous value.
- *
- * @param node the node to compute the size of.
- * @param dict the dictionary in which the word/attributes are to be found.
- * @param formatOptions file format options.
- * @return false if none of the cached addresses inside the node changed, true otherwise.
- */
- private static boolean computeActualNodeSize(final Node node, final FusionDictionary dict,
- final FormatOptions formatOptions) {
- boolean changed = false;
- int size = getGroupCountSize(node);
- for (CharGroup group : node.mData) {
- group.mCachedAddressAfterUpdate = node.mCachedAddressAfterUpdate + size;
- if (group.mCachedAddressAfterUpdate != group.mCachedAddressBeforeUpdate) {
- changed = true;
- }
- int groupSize = getGroupHeaderSize(group, formatOptions);
- if (group.isTerminal()) groupSize += FormatSpec.GROUP_FREQUENCY_SIZE;
- if (null == group.mChildren && formatOptions.mSupportsDynamicUpdate) {
- groupSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- } else if (null != group.mChildren) {
- if (formatOptions.mSupportsDynamicUpdate) {
- groupSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- } else {
- groupSize += getByteSize(getOffsetToTargetNodeDuringUpdate(node,
- groupSize + size, group.mChildren));
- }
- }
- groupSize += getShortcutListSize(group.mShortcutTargets);
- if (null != group.mBigrams) {
- for (WeightedString bigram : group.mBigrams) {
- final int offset = getOffsetToTargetCharGroupDuringUpdate(node,
- groupSize + size + FormatSpec.GROUP_FLAGS_SIZE,
- FusionDictionary.findWordInTree(dict.mRoot, bigram.mWord));
- groupSize += getByteSize(offset) + FormatSpec.GROUP_FLAGS_SIZE;
- }
- }
- group.mCachedSize = groupSize;
- size += groupSize;
- }
- if (formatOptions.mSupportsDynamicUpdate) {
- size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- if (node.mCachedSize != size) {
- node.mCachedSize = size;
- changed = true;
- }
- return changed;
- }
-
- /**
- * Initializes the cached addresses of nodes from their size.
- *
- * @param flatNodes the array of nodes.
- * @param formatOptions file format options.
- * @return the byte size of the entire stack.
- */
- private static int initializeNodesCachedAddresses(final ArrayList<Node> flatNodes,
- final FormatOptions formatOptions) {
- int nodeOffset = 0;
- for (final Node n : flatNodes) {
- n.mCachedAddressBeforeUpdate = nodeOffset;
- int groupCountSize = getGroupCountSize(n);
- int groupOffset = 0;
- for (final CharGroup g : n.mData) {
- g.mCachedAddressBeforeUpdate = g.mCachedAddressAfterUpdate =
- groupCountSize + nodeOffset + groupOffset;
- groupOffset += g.mCachedSize;
- }
- final int nodeSize = groupCountSize + groupOffset
- + (formatOptions.mSupportsDynamicUpdate
- ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
- nodeOffset += n.mCachedSize;
- }
- return nodeOffset;
- }
-
- /**
- * Updates the cached addresses of nodes after recomputing their new positions.
- *
- * @param flatNodes the array of nodes.
- */
- private static void updateNodeCachedAddresses(final ArrayList<Node> flatNodes) {
- for (final Node n : flatNodes) {
- n.mCachedAddressBeforeUpdate = n.mCachedAddressAfterUpdate;
- for (final CharGroup g : n.mData) {
- g.mCachedAddressBeforeUpdate = g.mCachedAddressAfterUpdate;
- }
- }
- }
-
- /**
- * Compute the cached parent addresses after all has been updated.
- *
- * The parent addresses are used by some binary formats at write-to-disk time. Not all formats
- * need them. In particular, version 2 does not need them, and version 3 does.
- *
- * @param flatNodes the flat array of nodes to fill in
- */
- private static void computeParentAddresses(final ArrayList<Node> flatNodes) {
- for (final Node node : flatNodes) {
- for (final CharGroup group : node.mData) {
- if (null != group.mChildren) {
- // Assign my address to children's parent address
- // Here BeforeUpdate and AfterUpdate addresses have the same value, so it
- // does not matter which we use.
- group.mChildren.mCachedParentAddress = group.mCachedAddressAfterUpdate
- - group.mChildren.mCachedAddressAfterUpdate;
- }
- }
- }
- }
-
- /**
- * Compute the addresses and sizes of an ordered node array.
- *
- * This method takes a node array and will update its cached address and size values
- * so that they can be written into a file. It determines the smallest size each of the
- * nodes can be given the addresses of its children and attributes, and store that into
- * each node.
- * The order of the node is given by the order of the array. This method makes no effort
- * to find a good order; it only mechanically computes the size this order results in.
- *
- * @param dict the dictionary
- * @param flatNodes the ordered array of nodes
- * @param formatOptions file format options.
- * @return the same array it was passed. The nodes have been updated for address and size.
- */
- private static ArrayList<Node> computeAddresses(final FusionDictionary dict,
- final ArrayList<Node> flatNodes, final FormatOptions formatOptions) {
- // First get the worst possible sizes and offsets
- for (final Node n : flatNodes) calculateNodeMaximumSize(n, formatOptions);
- final int offset = initializeNodesCachedAddresses(flatNodes, formatOptions);
-
- MakedictLog.i("Compressing the array addresses. Original size : " + offset);
- MakedictLog.i("(Recursively seen size : " + offset + ")");
-
- int passes = 0;
- boolean changesDone = false;
- do {
- changesDone = false;
- int nodeStartOffset = 0;
- for (final Node n : flatNodes) {
- n.mCachedAddressAfterUpdate = nodeStartOffset;
- final int oldNodeSize = n.mCachedSize;
- final boolean changed = computeActualNodeSize(n, dict, formatOptions);
- final int newNodeSize = n.mCachedSize;
- if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
- nodeStartOffset += newNodeSize;
- changesDone |= changed;
- }
- updateNodeCachedAddresses(flatNodes);
- ++passes;
- if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
- } while (changesDone);
-
- if (formatOptions.mSupportsDynamicUpdate) {
- computeParentAddresses(flatNodes);
- }
- final Node lastNode = flatNodes.get(flatNodes.size() - 1);
- MakedictLog.i("Compression complete in " + passes + " passes.");
- MakedictLog.i("After address compression : "
- + (lastNode.mCachedAddressAfterUpdate + lastNode.mCachedSize));
-
- return flatNodes;
- }
-
- /**
- * Sanity-checking method.
- *
- * This method checks an array of node for juxtaposition, that is, it will do
- * nothing if each node's cached address is actually the previous node's address
- * plus the previous node's size.
- * If this is not the case, it will throw an exception.
- *
- * @param array the array node to check
- */
- private static void checkFlatNodeArray(final ArrayList<Node> array) {
- int offset = 0;
- int index = 0;
- for (final Node n : array) {
- // BeforeUpdate and AfterUpdate addresses are the same here, so it does not matter
- // which we use.
- if (n.mCachedAddressAfterUpdate != offset) {
- throw new RuntimeException("Wrong address for node " + index
- + " : expected " + offset + ", got " + n.mCachedAddressAfterUpdate);
- }
- ++index;
- offset += n.mCachedSize;
- }
- }
-
- /**
- * Helper method to write a variable-size address to a file.
- *
- * @param buffer the buffer to write to.
- * @param index the index in the buffer to write the address to.
- * @param address the address to write.
- * @return the size in bytes the address actually took.
- */
- private static int writeVariableAddress(final byte[] buffer, int index, final int address) {
- switch (getByteSize(address)) {
- case 1:
- buffer[index++] = (byte)address;
- return 1;
- case 2:
- buffer[index++] = (byte)(0xFF & (address >> 8));
- buffer[index++] = (byte)(0xFF & address);
- return 2;
- case 3:
- buffer[index++] = (byte)(0xFF & (address >> 16));
- buffer[index++] = (byte)(0xFF & (address >> 8));
- buffer[index++] = (byte)(0xFF & address);
- return 3;
- case 0:
- return 0;
- default:
- throw new RuntimeException("Address " + address + " has a strange size");
- }
- }
-
- /**
- * Helper method to write a variable-size signed address to a file.
- *
- * @param buffer the buffer to write to.
- * @param index the index in the buffer to write the address to.
- * @param address the address to write.
- * @return the size in bytes the address actually took.
- */
- private static int writeVariableSignedAddress(final byte[] buffer, int index,
- final int address) {
- if (!hasChildrenAddress(address)) {
- buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
- } else {
- final int absAddress = Math.abs(address);
- buffer[index++] = (byte)((address < 0 ? MSB8 : 0) | (0xFF & (absAddress >> 16)));
- buffer[index++] = (byte)(0xFF & (absAddress >> 8));
- buffer[index++] = (byte)(0xFF & absAddress);
- }
- return 3;
- }
-
- /**
- * Makes the flag value for a char group.
- *
- * @param hasMultipleChars whether the group has multiple chars.
- * @param isTerminal whether the group is terminal.
- * @param childrenAddressSize the size of a children address.
- * @param hasShortcuts whether the group has shortcuts.
- * @param hasBigrams whether the group has bigrams.
- * @param isNotAWord whether the group is not a word.
- * @param isBlackListEntry whether the group is a blacklist entry.
- * @param formatOptions file format options.
- * @return the flags
- */
- static int makeCharGroupFlags(final boolean hasMultipleChars, final boolean isTerminal,
- final int childrenAddressSize, final boolean hasShortcuts, final boolean hasBigrams,
- final boolean isNotAWord, final boolean isBlackListEntry,
- final FormatOptions formatOptions) {
- byte flags = 0;
- if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS;
- if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL;
- if (formatOptions.mSupportsDynamicUpdate) {
- flags |= FormatSpec.FLAG_IS_NOT_MOVED;
- } else if (true) {
- switch (childrenAddressSize) {
- case 1:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
- break;
- case 0:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS;
- break;
- default:
- throw new RuntimeException("Node with a strange address");
- }
- }
- if (hasShortcuts) flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS;
- if (hasBigrams) flags |= FormatSpec.FLAG_HAS_BIGRAMS;
- if (isNotAWord) flags |= FormatSpec.FLAG_IS_NOT_A_WORD;
- if (isBlackListEntry) flags |= FormatSpec.FLAG_IS_BLACKLISTED;
- return flags;
- }
-
- private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
- final int childrenOffset, final FormatOptions formatOptions) {
- return (byte) makeCharGroupFlags(group.mChars.length > 1, group.mFrequency >= 0,
- getByteSize(childrenOffset), group.mShortcutTargets != null, group.mBigrams != null,
- group.mIsNotAWord, group.mIsBlacklistEntry, formatOptions);
- }
-
- /**
- * Makes the flag value for a bigram.
- *
- * @param more whether there are more bigrams after this one.
- * @param offset the offset of the bigram.
- * @param bigramFrequency the frequency of the bigram, 0..255.
- * @param unigramFrequency the unigram frequency of the same word, 0..255.
- * @param word the second bigram, for debugging purposes
- * @return the flags
- */
- private static final int makeBigramFlags(final boolean more, final int offset,
- int bigramFrequency, final int unigramFrequency, final String word) {
- int bigramFlags = (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
- + (offset < 0 ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
- switch (getByteSize(offset)) {
- case 1:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
- break;
- default:
- throw new RuntimeException("Strange offset size");
- }
- if (unigramFrequency > bigramFrequency) {
- MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word
- + "\". Bigram freq is " + bigramFrequency + ", unigram freq for "
- + word + " is " + unigramFrequency);
- bigramFrequency = unigramFrequency;
- }
- // We compute the difference between 255 (which means probability = 1) and the
- // unigram score. We split this into a number of discrete steps.
- // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15
- // represents an increase of 16 steps: a value of 15 will be interpreted as the median
- // value of the 16th step. In all justice, if the bigram frequency is low enough to be
- // rounded below the first step (which means it is less than half a step higher than the
- // unigram frequency) then the unigram frequency itself is the best approximation of the
- // bigram freq that we could possibly supply, hence we should *not* include this bigram
- // in the file at all.
- // until this is done, we'll write 0 and slightly overestimate this case.
- // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step
- // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to
- // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the
- // step size. Then we compute the start of the first step (the one where value 0 starts)
- // by adding half-a-step to the unigramFrequency. From there, we compute the integer
- // number of steps to the bigramFrequency. One last thing: we want our steps to include
- // their lower bound and exclude their higher bound so we need to have the first step
- // start at exactly 1 unit higher than floor(unigramFreq + half a step).
- // Note : to reconstruct the score, the dictionary reader will need to divide
- // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step,
- // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best
- // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the
- // step pointed by the discretized frequency.
- final float stepSize =
- (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
- / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY);
- final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
- final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
- // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
- // here. The best approximation would be the unigram freq itself, so we should not
- // include this bigram in the dictionary. For now, register as 0, and live with the
- // small over-estimation that we get in this case. TODO: actually remove this bigram
- // if discretizedFrequency < 0.
- final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0;
- bigramFlags += finalBigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY;
- return bigramFlags;
- }
-
- /**
- * Makes the 2-byte value for options flags.
- */
- private static final int makeOptionsValue(final FusionDictionary dictionary,
- final FormatOptions formatOptions) {
- final DictionaryOptions options = dictionary.mOptions;
- final boolean hasBigrams = dictionary.hasBigrams();
- return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0)
- + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0)
- + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0)
- + (formatOptions.mSupportsDynamicUpdate ? FormatSpec.SUPPORTS_DYNAMIC_UPDATE : 0);
- }
-
- /**
- * Makes the flag value for a shortcut.
- *
- * @param more whether there are more attributes after this one.
- * @param frequency the frequency of the attribute, 0..15
- * @return the flags
- */
- static final int makeShortcutFlags(final boolean more, final int frequency) {
- return (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
- + (frequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY);
- }
-
- private static final int writeParentAddress(final byte[] buffer, final int index,
- final int address, final FormatOptions formatOptions) {
- if (supportsDynamicUpdate(formatOptions)) {
- if (address == FormatSpec.NO_PARENT_ADDRESS) {
- buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
- } else {
- final int absAddress = Math.abs(address);
- assert(absAddress <= SINT24_MAX);
- buffer[index] = (byte)((address < 0 ? MSB8 : 0)
- | ((absAddress >> 16) & 0xFF));
- buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF);
- buffer[index + 2] = (byte)(absAddress & 0xFF);
- }
- return index + 3;
- } else {
- return index;
- }
- }
-
- /**
- * Write a node to memory. The node is expected to have its final position cached.
- *
- * This can be an empty map, but the more is inside the faster the lookups will be. It can
- * be carried on as long as nodes do not move.
- *
- * @param dict the dictionary the node is a part of (for relative offsets).
- * @param buffer the memory buffer to write to.
- * @param node the node to write.
- * @param formatOptions file format options.
- * @return the address of the END of the node.
- */
- @SuppressWarnings("unused")
- private static int writePlacedNode(final FusionDictionary dict, byte[] buffer,
- final Node node, final FormatOptions formatOptions) {
- // TODO: Make the code in common with BinaryDictIOUtils#writeCharGroup
- int index = node.mCachedAddressAfterUpdate;
-
- final int groupCount = node.mData.size();
- final int countSize = getGroupCountSize(node);
- final int parentAddress = node.mCachedParentAddress;
- if (1 == countSize) {
- buffer[index++] = (byte)groupCount;
- } else if (2 == countSize) {
- // We need to signal 2-byte size by setting the top bit of the MSB to 1, so
- // we | 0x80 to do this.
- buffer[index++] = (byte)((groupCount >> 8) | 0x80);
- buffer[index++] = (byte)(groupCount & 0xFF);
- } else {
- throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
- }
- int groupAddress = index;
- for (int i = 0; i < groupCount; ++i) {
- final CharGroup group = node.mData.get(i);
- if (index != group.mCachedAddressAfterUpdate) {
- throw new RuntimeException("Bug: write index is not the same as the cached address "
- + "of the group : " + index + " <> " + group.mCachedAddressAfterUpdate);
- }
- groupAddress += getGroupHeaderSize(group, formatOptions);
- // Sanity checks.
- if (DBG && group.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) {
- throw new RuntimeException("A node has a frequency > "
- + FormatSpec.MAX_TERMINAL_FREQUENCY
- + " : " + group.mFrequency);
- }
- if (group.mFrequency >= 0) groupAddress += FormatSpec.GROUP_FREQUENCY_SIZE;
- final int childrenOffset = null == group.mChildren
- ? FormatSpec.NO_CHILDREN_ADDRESS
- : group.mChildren.mCachedAddressAfterUpdate - groupAddress;
- buffer[index++] =
- makeCharGroupFlags(group, groupAddress, childrenOffset, formatOptions);
-
- if (parentAddress == FormatSpec.NO_PARENT_ADDRESS) {
- index = writeParentAddress(buffer, index, parentAddress, formatOptions);
- } else {
- index = writeParentAddress(buffer, index, parentAddress
- + (node.mCachedAddressAfterUpdate - group.mCachedAddressAfterUpdate),
- formatOptions);
- }
-
- index = CharEncoding.writeCharArray(group.mChars, buffer, index);
- if (group.hasSeveralChars()) {
- buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
- }
- if (group.mFrequency >= 0) {
- buffer[index++] = (byte) group.mFrequency;
- }
-
- final int shift;
- if (formatOptions.mSupportsDynamicUpdate) {
- shift = writeVariableSignedAddress(buffer, index, childrenOffset);
- } else {
- shift = writeVariableAddress(buffer, index, childrenOffset);
- }
- index += shift;
- groupAddress += shift;
-
- // Write shortcuts
- if (null != group.mShortcutTargets) {
- final int indexOfShortcutByteSize = index;
- index += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- groupAddress += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- final Iterator<WeightedString> shortcutIterator = group.mShortcutTargets.iterator();
- while (shortcutIterator.hasNext()) {
- final WeightedString target = shortcutIterator.next();
- ++groupAddress;
- int shortcutFlags = makeShortcutFlags(shortcutIterator.hasNext(),
- target.mFrequency);
- buffer[index++] = (byte)shortcutFlags;
- final int shortcutShift = CharEncoding.writeString(buffer, index, target.mWord);
- index += shortcutShift;
- groupAddress += shortcutShift;
- }
- final int shortcutByteSize = index - indexOfShortcutByteSize;
- if (shortcutByteSize > 0xFFFF) {
- throw new RuntimeException("Shortcut list too large");
- }
- buffer[indexOfShortcutByteSize] = (byte)(shortcutByteSize >> 8);
- buffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF);
- }
- // Write bigrams
- if (null != group.mBigrams) {
- final Iterator<WeightedString> bigramIterator = group.mBigrams.iterator();
- while (bigramIterator.hasNext()) {
- final WeightedString bigram = bigramIterator.next();
- final CharGroup target =
- FusionDictionary.findWordInTree(dict.mRoot, bigram.mWord);
- final int addressOfBigram = target.mCachedAddressAfterUpdate;
- final int unigramFrequencyForThisWord = target.mFrequency;
- ++groupAddress;
- final int offset = addressOfBigram - groupAddress;
- int bigramFlags = makeBigramFlags(bigramIterator.hasNext(), offset,
- bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
- buffer[index++] = (byte)bigramFlags;
- final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
- index += bigramShift;
- groupAddress += bigramShift;
- }
- }
-
- }
- if (formatOptions.mSupportsDynamicUpdate) {
- buffer[index] = buffer[index + 1] = buffer[index + 2]
- = FormatSpec.NO_FORWARD_LINK_ADDRESS;
- index += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- if (index != node.mCachedAddressAfterUpdate + node.mCachedSize) throw new RuntimeException(
- "Not the same size : written "
- + (index - node.mCachedAddressAfterUpdate) + " bytes from a node that should have "
- + node.mCachedSize + " bytes");
- return index;
- }
-
- /**
- * Dumps a collection of useful statistics about a node array.
- *
- * This prints purely informative stuff, like the total estimated file size, the
- * number of nodes, of character groups, the repartition of each address size, etc
- *
- * @param nodes the node array.
- */
- private static void showStatistics(ArrayList<Node> nodes) {
- int firstTerminalAddress = Integer.MAX_VALUE;
- int lastTerminalAddress = Integer.MIN_VALUE;
- int size = 0;
- int charGroups = 0;
- int maxGroups = 0;
- int maxRuns = 0;
- for (final Node n : nodes) {
- if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
- for (final CharGroup cg : n.mData) {
- ++charGroups;
- if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
- if (cg.mFrequency >= 0) {
- if (n.mCachedAddressAfterUpdate < firstTerminalAddress)
- firstTerminalAddress = n.mCachedAddressAfterUpdate;
- if (n.mCachedAddressAfterUpdate > lastTerminalAddress)
- lastTerminalAddress = n.mCachedAddressAfterUpdate;
- }
- }
- if (n.mCachedAddressAfterUpdate + n.mCachedSize > size) {
- size = n.mCachedAddressAfterUpdate + n.mCachedSize;
- }
- }
- final int[] groupCounts = new int[maxGroups + 1];
- final int[] runCounts = new int[maxRuns + 1];
- for (final Node n : nodes) {
- ++groupCounts[n.mData.size()];
- for (final CharGroup cg : n.mData) {
- ++runCounts[cg.mChars.length];
- }
- }
-
- MakedictLog.i("Statistics:\n"
- + " total file size " + size + "\n"
- + " " + nodes.size() + " nodes\n"
- + " " + charGroups + " groups (" + ((float)charGroups / nodes.size())
- + " groups per node)\n"
- + " first terminal at " + firstTerminalAddress + "\n"
- + " last terminal at " + lastTerminalAddress + "\n"
- + " Group stats : max = " + maxGroups);
- for (int i = 0; i < groupCounts.length; ++i) {
- MakedictLog.i(" " + i + " : " + groupCounts[i]);
- }
- MakedictLog.i(" Character run stats : max = " + maxRuns);
- for (int i = 0; i < runCounts.length; ++i) {
- MakedictLog.i(" " + i + " : " + runCounts[i]);
- }
- }
-
- /**
- * Dumps a FusionDictionary to a file.
- *
- * This is the public entry point to write a dictionary to a file.
- *
- * @param destination the stream to write the binary data to.
- * @param dict the dictionary to write.
- * @param formatOptions file format options.
- */
- public static void writeDictionaryBinary(final OutputStream destination,
- final FusionDictionary dict, final FormatOptions formatOptions)
- throws IOException, UnsupportedFormatException {
-
- // Addresses are limited to 3 bytes, but since addresses can be relative to each node, the
- // structure itself is not limited to 16MB. However, if it is over 16MB deciding the order
- // of the nodes becomes a quite complicated problem, because though the dictionary itself
- // does not have a size limit, each node must still be within 16MB of all its children and
- // parents. As long as this is ensured, the dictionary file may grow to any size.
-
- final int version = formatOptions.mVersion;
- if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
- || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
- throw new UnsupportedFormatException("Requested file format version " + version
- + ", but this implementation only supports versions "
- + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through "
- + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
- }
-
- ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
-
- // The magic number in big-endian order.
- if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- // Magic number for version 2+.
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 24)));
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 16)));
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_2_MAGIC_NUMBER));
- // Dictionary version.
- headerBuffer.write((byte) (0xFF & (version >> 8)));
- headerBuffer.write((byte) (0xFF & version));
- } else {
- // Magic number for version 1.
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_1_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_1_MAGIC_NUMBER));
- // Dictionary version.
- headerBuffer.write((byte) (0xFF & version));
- }
- // Options flags
- final int options = makeOptionsValue(dict, formatOptions);
- headerBuffer.write((byte) (0xFF & (options >> 8)));
- headerBuffer.write((byte) (0xFF & options));
- if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- final int headerSizeOffset = headerBuffer.size();
- // Placeholder to be written later with header size.
- for (int i = 0; i < 4; ++i) {
- headerBuffer.write(0);
- }
- // Write out the options.
- for (final String key : dict.mOptions.mAttributes.keySet()) {
- final String value = dict.mOptions.mAttributes.get(key);
- CharEncoding.writeString(headerBuffer, key);
- CharEncoding.writeString(headerBuffer, value);
- }
- final int size = headerBuffer.size();
- final byte[] bytes = headerBuffer.toByteArray();
- // Write out the header size.
- bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24));
- bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16));
- bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8));
- bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0));
- destination.write(bytes);
- } else {
- headerBuffer.writeTo(destination);
- }
-
- headerBuffer.close();
-
- // Leave the choice of the optimal node order to the flattenTree function.
- MakedictLog.i("Flattening the tree...");
- ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
-
- MakedictLog.i("Computing addresses...");
- computeAddresses(dict, flatNodes, formatOptions);
- MakedictLog.i("Checking array...");
- if (DBG) checkFlatNodeArray(flatNodes);
-
- // Create a buffer that matches the final dictionary size.
- final Node lastNode = flatNodes.get(flatNodes.size() - 1);
- final int bufferSize = lastNode.mCachedAddressAfterUpdate + lastNode.mCachedSize;
- final byte[] buffer = new byte[bufferSize];
- int index = 0;
-
- MakedictLog.i("Writing file...");
- int dataEndOffset = 0;
- for (Node n : flatNodes) {
- dataEndOffset = writePlacedNode(dict, buffer, n, formatOptions);
- }
-
- if (DBG) showStatistics(flatNodes);
-
- destination.write(buffer, 0, dataEndOffset);
-
- destination.close();
- MakedictLog.i("Done");
- }
-
-
- // Input methods: Read a binary dictionary to memory.
- // readDictionaryBinary is the public entry point for them.
-
- static int getChildrenAddressSize(final int optionFlags,
- final FormatOptions formatOptions) {
- if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- switch (optionFlags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) {
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- return 1;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- return 2;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- return 3;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
- default:
- return 0;
- }
- }
-
- static int readChildrenAddress(final FusionDictionaryBufferInterface buffer,
- final int optionFlags, final FormatOptions options) {
- if (options.mSupportsDynamicUpdate) {
- final int address = buffer.readUnsignedInt24();
- if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
- if ((address & MSB24) != 0) {
- return -(address & SINT24_MAX);
- } else {
- return address;
- }
- }
- int address;
- switch (optionFlags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) {
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- return buffer.readUnsignedByte();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- return buffer.readUnsignedShort();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- return buffer.readUnsignedInt24();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
- default:
- return FormatSpec.NO_CHILDREN_ADDRESS;
- }
- }
-
- static int readParentAddress(final FusionDictionaryBufferInterface buffer,
- final FormatOptions formatOptions) {
- if (supportsDynamicUpdate(formatOptions)) {
- final int parentAddress = buffer.readUnsignedInt24();
- final int sign = ((parentAddress & MSB24) != 0) ? -1 : 1;
- return sign * (parentAddress & SINT24_MAX);
- } else {
- return FormatSpec.NO_PARENT_ADDRESS;
- }
- }
-
- private static final int[] CHARACTER_BUFFER = new int[FormatSpec.MAX_WORD_LENGTH];
- public static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer,
- final int originalGroupAddress, final FormatOptions options) {
- int addressPointer = originalGroupAddress;
- final int flags = buffer.readUnsignedByte();
- ++addressPointer;
-
- final int parentAddress = readParentAddress(buffer, options);
- if (supportsDynamicUpdate(options)) {
- addressPointer += 3;
- }
-
- final int characters[];
- if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) {
- int index = 0;
- int character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- while (-1 != character) {
- // FusionDictionary is making sure that the length of the word is smaller than
- // MAX_WORD_LENGTH.
- // So we'll never write past the end of CHARACTER_BUFFER.
- CHARACTER_BUFFER[index++] = character;
- character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- }
- characters = Arrays.copyOfRange(CHARACTER_BUFFER, 0, index);
- } else {
- final int character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- characters = new int[] { character };
- }
- final int frequency;
- if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
- ++addressPointer;
- frequency = buffer.readUnsignedByte();
- } else {
- frequency = CharGroup.NOT_A_TERMINAL;
- }
- int childrenAddress = readChildrenAddress(buffer, flags, options);
- if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- childrenAddress += addressPointer;
- }
- addressPointer += getChildrenAddressSize(flags, options);
- ArrayList<WeightedString> shortcutTargets = null;
- if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
- final int pointerBefore = buffer.position();
- shortcutTargets = new ArrayList<WeightedString>();
- buffer.readUnsignedShort(); // Skip the size
- while (true) {
- final int targetFlags = buffer.readUnsignedByte();
- final String word = CharEncoding.readString(buffer);
- shortcutTargets.add(new WeightedString(word,
- targetFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY));
- if (0 == (targetFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
- }
- addressPointer += buffer.position() - pointerBefore;
- }
- ArrayList<PendingAttribute> bigrams = null;
- if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
- bigrams = new ArrayList<PendingAttribute>();
- int bigramCount = 0;
- while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- final int bigramFlags = buffer.readUnsignedByte();
- ++addressPointer;
- final int sign = 0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE)
- ? 1 : -1;
- int bigramAddress = addressPointer;
- switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- bigramAddress += sign * buffer.readUnsignedByte();
- addressPointer += 1;
- break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- bigramAddress += sign * buffer.readUnsignedShort();
- addressPointer += 2;
- break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- final int offset = (buffer.readUnsignedByte() << 16)
- + buffer.readUnsignedShort();
- bigramAddress += sign * offset;
- addressPointer += 3;
- break;
- default:
- throw new RuntimeException("Has bigrams with no address");
- }
- bigrams.add(new PendingAttribute(bigramFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY,
- bigramAddress));
- if (0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
- }
- if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- MakedictLog.d("too many bigrams in a group.");
- }
- }
- return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
- parentAddress, childrenAddress, shortcutTargets, bigrams);
- }
-
- /**
- * Reads and returns the char group count out of a buffer and forwards the pointer.
- */
- public static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) {
- final int msb = buffer.readUnsignedByte();
- if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
- return msb;
- } else {
- return ((FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
- + buffer.readUnsignedByte();
- }
- }
-
- // The word cache here is a stopgap bandaid to help the catastrophic performance
- // of this method. Since it performs direct, unbuffered random access to the file and
- // may be called hundreds of thousands of times, the resulting performance is not
- // reasonable without some kind of cache. Thus:
- private static TreeMap<Integer, WeightedString> wordCache =
- new TreeMap<Integer, WeightedString>();
- /**
- * Finds, as a string, the word at the address passed as an argument.
- *
- * @param buffer the buffer to read from.
- * @param headerSize the size of the header.
- * @param address the address to seek.
- * @param formatOptions file format options.
- * @return the word with its frequency, as a weighted string.
- */
- /* package for tests */ static WeightedString getWordAtAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions formatOptions) {
- final WeightedString cachedString = wordCache.get(address);
- if (null != cachedString) return cachedString;
-
- final WeightedString result;
- final int originalPointer = buffer.position();
- buffer.position(address);
-
- if (supportsDynamicUpdate(formatOptions)) {
- result = getWordAtAddressWithParentAddress(buffer, headerSize, address, formatOptions);
- } else {
- result = getWordAtAddressWithoutParentAddress(buffer, headerSize, address,
- formatOptions);
- }
-
- wordCache.put(address, result);
- buffer.position(originalPointer);
- return result;
- }
-
- // TODO: static!? This will behave erratically when used in multi-threaded code.
- // We need to fix this
- private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
- @SuppressWarnings("unused")
- private static WeightedString getWordAtAddressWithParentAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions options) {
- int currentAddress = address;
- int index = FormatSpec.MAX_WORD_LENGTH - 1;
- int frequency = Integer.MIN_VALUE;
- // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH
- for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) {
- CharGroupInfo currentInfo;
- int loopCounter = 0;
- do {
- buffer.position(currentAddress + headerSize);
- currentInfo = readCharGroup(buffer, currentAddress, options);
- if (isMovedGroup(currentInfo.mFlags, options)) {
- currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
- }
- if (DBG && loopCounter++ > MAX_JUMPS) {
- MakedictLog.d("Too many jumps - probably a bug");
- }
- } while (isMovedGroup(currentInfo.mFlags, options));
- if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency;
- for (int i = 0; i < currentInfo.mCharacters.length; ++i) {
- sGetWordBuffer[index--] =
- currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1];
- }
- if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break;
- currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
- }
-
- return new WeightedString(
- new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1),
- frequency);
- }
-
- private static WeightedString getWordAtAddressWithoutParentAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions options) {
- buffer.position(headerSize);
- final int count = readCharGroupCount(buffer);
- int groupOffset = getGroupCountSize(count);
- final StringBuilder builder = new StringBuilder();
- WeightedString result = null;
-
- CharGroupInfo last = null;
- for (int i = count - 1; i >= 0; --i) {
- CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
- groupOffset = info.mEndAddress;
- if (info.mOriginalAddress == address) {
- builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
- result = new WeightedString(builder.toString(), info.mFrequency);
- break; // and return
- }
- if (hasChildrenAddress(info.mChildrenAddress)) {
- if (info.mChildrenAddress > address) {
- if (null == last) continue;
- builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- buffer.position(last.mChildrenAddress + headerSize);
- i = readCharGroupCount(buffer);
- groupOffset = last.mChildrenAddress + getGroupCountSize(i);
- last = null;
- continue;
- }
- last = info;
- }
- if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
- builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- buffer.position(last.mChildrenAddress + headerSize);
- i = readCharGroupCount(buffer);
- groupOffset = last.mChildrenAddress + getGroupCountSize(i);
- last = null;
- continue;
- }
- }
- return result;
- }
-
- /**
- * Reads a single node from a buffer.
- *
- * This methods reads the file at the current position. A node is fully expected to start at
- * the current position.
- * This will recursively read other nodes into the structure, populating the reverse
- * maps on the fly and using them to keep track of already read nodes.
- *
- * @param buffer the buffer, correctly positioned at the start of a node.
- * @param headerSize the size, in bytes, of the file header.
- * @param reverseNodeMap a mapping from addresses to already read nodes.
- * @param reverseGroupMap a mapping from addresses to already read character groups.
- * @param options file format options.
- * @return the read node with all his children already read.
- */
- private static Node readNode(final FusionDictionaryBufferInterface buffer, final int headerSize,
- final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap,
- final FormatOptions options)
- throws IOException {
- final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
- final int nodeOrigin = buffer.position() - headerSize;
-
- do { // Scan the linked-list node.
- final int nodeHeadPosition = buffer.position() - headerSize;
- final int count = readCharGroupCount(buffer);
- int groupOffset = nodeHeadPosition + getGroupCountSize(count);
- for (int i = count; i > 0; --i) { // Scan the array of CharGroup.
- CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
- if (isMovedGroup(info.mFlags, options)) continue;
- ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
- ArrayList<WeightedString> bigrams = null;
- if (null != info.mBigrams) {
- bigrams = new ArrayList<WeightedString>();
- for (PendingAttribute bigram : info.mBigrams) {
- final WeightedString word = getWordAtAddress(
- buffer, headerSize, bigram.mAddress, options);
- final int reconstructedFrequency =
- reconstructBigramFrequency(word.mFrequency, bigram.mFrequency);
- bigrams.add(new WeightedString(word.mWord, reconstructedFrequency));
- }
- }
- if (hasChildrenAddress(info.mChildrenAddress)) {
- Node children = reverseNodeMap.get(info.mChildrenAddress);
- if (null == children) {
- final int currentPosition = buffer.position();
- buffer.position(info.mChildrenAddress + headerSize);
- children = readNode(
- buffer, headerSize, reverseNodeMap, reverseGroupMap, options);
- buffer.position(currentPosition);
- }
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams,
- info.mFrequency,
- 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
- 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children));
- } else {
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams,
- info.mFrequency,
- 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
- 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
- }
- groupOffset = info.mEndAddress;
- }
-
- // reach the end of the array.
- if (options.mSupportsDynamicUpdate) {
- final int nextAddress = buffer.readUnsignedInt24();
- if (nextAddress >= 0 && nextAddress < buffer.limit()) {
- buffer.position(nextAddress);
- } else {
- break;
- }
- }
- } while (options.mSupportsDynamicUpdate &&
- buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
-
- final Node node = new Node(nodeContents);
- node.mCachedAddressBeforeUpdate = nodeOrigin;
- node.mCachedAddressAfterUpdate = nodeOrigin;
- reverseNodeMap.put(node.mCachedAddressAfterUpdate, node);
- return node;
- }
-
- /**
- * Helper function to get the binary format version from the header.
- * @throws IOException
- */
- private static int getFormatVersion(final FusionDictionaryBufferInterface buffer)
- throws IOException {
- final int magic_v1 = buffer.readUnsignedShort();
- if (FormatSpec.VERSION_1_MAGIC_NUMBER == magic_v1) return buffer.readUnsignedByte();
- final int magic_v2 = (magic_v1 << 16) + buffer.readUnsignedShort();
- if (FormatSpec.VERSION_2_MAGIC_NUMBER == magic_v2) return buffer.readUnsignedShort();
- return FormatSpec.NOT_A_VERSION_NUMBER;
- }
-
- /**
- * Helper function to get and validate the binary format version.
- * @throws UnsupportedFormatException
- * @throws IOException
- */
- private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer)
- throws IOException, UnsupportedFormatException {
- final int version = getFormatVersion(buffer);
- if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
- || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
- throw new UnsupportedFormatException("This file has version " + version
- + ", but this implementation does not support versions above "
- + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
- }
- return version;
- }
-
- /**
- * Reads a header from a buffer.
- * @param buffer the buffer to read.
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer)
- throws IOException, UnsupportedFormatException {
- final int version = checkFormatVersion(buffer);
- final int optionsFlags = buffer.readUnsignedShort();
-
- final HashMap<String, String> attributes = new HashMap<String, String>();
- final int headerSize;
- if (version < FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- headerSize = buffer.position();
- } else {
- headerSize = buffer.readInt();
- populateOptions(buffer, headerSize, attributes);
- buffer.position(headerSize);
- }
-
- if (headerSize < 0) {
- throw new UnsupportedFormatException("header size can't be negative.");
- }
-
- final FileHeader header = new FileHeader(headerSize,
- new FusionDictionary.DictionaryOptions(attributes,
- 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
- 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
- new FormatOptions(version,
- 0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE)));
- return header;
- }
-
- /**
- * Reads options from a buffer and populate a map with their contents.
- *
- * The buffer is read at the current position, so the caller must take care the pointer
- * is in the right place before calling this.
- */
- public static void populateOptions(final FusionDictionaryBufferInterface buffer,
- final int headerSize, final HashMap<String, String> options) {
- while (buffer.position() < headerSize) {
- final String key = CharEncoding.readString(buffer);
- final String value = CharEncoding.readString(buffer);
- options.put(key, value);
- }
- }
-
- /**
- * Reads a buffer and returns the memory representation of the dictionary.
- *
- * This high-level method takes a buffer and reads its contents, populating a
- * FusionDictionary structure. The optional dict argument is an existing dictionary to
- * which words from the buffer should be added. If it is null, a new dictionary is created.
- *
- * @param buffer the buffer to read.
- * @param dict an optional dictionary to add words to, or null.
- * @return the created (or merged) dictionary.
- */
- @UsedForTesting
- public static FusionDictionary readDictionaryBinary(
- final FusionDictionaryBufferInterface buffer, final FusionDictionary dict)
- throws IOException, UnsupportedFormatException {
- // clear cache
- wordCache.clear();
-
- // Read header
- final FileHeader header = readHeader(buffer);
-
- Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
- Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
- final Node root = readNode(buffer, header.mHeaderSize, reverseNodeMapping,
- reverseGroupMapping, header.mFormatOptions);
-
- FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions);
- if (null != dict) {
- for (final Word w : dict) {
- if (w.mIsBlacklistEntry) {
- newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord);
- } else {
- newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord);
- }
- }
- for (final Word w : dict) {
- // By construction a binary dictionary may not have bigrams pointing to
- // words that are not also registered as unigrams so we don't have to avoid
- // them explicitly here.
- for (final WeightedString bigram : w.mBigrams) {
- newDict.setBigram(w.mWord, bigram.mWord, bigram.mFrequency);
- }
- }
- }
-
- return newDict;
- }
-
- /**
- * Helper method to pass a file name instead of a File object to isBinaryDictionary.
- */
- public static boolean isBinaryDictionary(final String filename) {
- final File file = new File(filename);
- return isBinaryDictionary(file);
- }
-
- /**
- * Basic test to find out whether the file is a binary dictionary or not.
- *
- * Concretely this only tests the magic number.
- *
- * @param file The file to test.
- * @return true if it's a binary dictionary, false otherwise
- */
- public static boolean isBinaryDictionary(final File file) {
- FileInputStream inStream = null;
- try {
- inStream = new FileInputStream(file);
- final ByteBuffer buffer = inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, 0, file.length());
- final int version = getFormatVersion(new ByteBufferWrapper(buffer));
- return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION
- && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION);
- } catch (FileNotFoundException e) {
- return false;
- } catch (IOException e) {
- return false;
- } finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
- }
- }
-
- /**
- * Calculate bigram frequency from compressed value
- *
- * @see #makeBigramFlags
- *
- * @param unigramFrequency
- * @param bigramFrequency compressed frequency
- * @return approximate bigram frequency
- */
- public static int reconstructBigramFrequency(final int unigramFrequency,
- final int bigramFrequency) {
- final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency)
- / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY);
- final float resultFreqFloat = unigramFrequency + stepSize * (bigramFrequency + 1.0f);
- return (int)resultFreqFloat;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
new file mode 100644
index 000000000..11a3f0b3a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
@@ -0,0 +1,186 @@
+/*
+ * 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.BinaryDictDecoderUtils.DictBuffer;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.utils.ByteArrayDictBuffer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.TreeMap;
+
+/**
+ * An interface of binary dictionary decoder.
+ */
+public interface DictDecoder {
+ public FileHeader readHeader() throws IOException, UnsupportedFormatException;
+
+ /**
+ * Reads PtNode from nodeAddress.
+ * @param ptNodePos the position of PtNode.
+ * @param formatOptions the format options.
+ * @return PtNodeInfo.
+ */
+ public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions formatOptions);
+
+ /**
+ * Reads a buffer and returns the memory representation of the dictionary.
+ *
+ * This high-level method takes a buffer and reads its contents, populating a
+ * FusionDictionary structure. The optional dict argument is an existing dictionary to
+ * which words from the buffer should be added. If it is null, a new dictionary is created.
+ *
+ * @param dict an optional dictionary to add words to, or null.
+ * @return the created (or merged) dictionary.
+ */
+ @UsedForTesting
+ public FusionDictionary readDictionaryBinary(final FusionDictionary dict)
+ throws FileNotFoundException, IOException, UnsupportedFormatException;
+
+ /**
+ * Gets the address of the last PtNode of the exact matching word in the dictionary.
+ * If no match is found, returns NOT_VALID_WORD.
+ *
+ * @param word the word we search for.
+ * @return the address of the terminal node.
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
+ */
+ @UsedForTesting
+ public int getTerminalPosition(final String word)
+ throws IOException, UnsupportedFormatException;
+
+ /**
+ * Reads unigrams and bigrams from the binary file.
+ * Doesn't store a full memory representation of the dictionary.
+ *
+ * @param words the map to store the address as a key and the word as a value.
+ * @param frequencies the map to store the address as a key and the frequency as a value.
+ * @param bigrams the map to store the address as a key and the list of address as a value.
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
+ */
+ public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words,
+ final TreeMap<Integer, Integer> frequencies,
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams)
+ throws IOException, UnsupportedFormatException;
+
+ // Flags for DictionaryBufferFactory.
+ public static final int USE_READONLY_BYTEBUFFER = 0x01000000;
+ public static final int USE_BYTEARRAY = 0x02000000;
+ public static final int USE_WRITABLE_BYTEBUFFER = 0x04000000;
+ public static final int MASK_DICTBUFFER = 0x0F000000;
+
+ public interface DictionaryBufferFactory {
+ public DictBuffer getDictionaryBuffer(final File file)
+ throws FileNotFoundException, IOException;
+ }
+
+ /**
+ * Creates DictionaryBuffer using a ByteBuffer
+ *
+ * This class uses less memory than DictionaryBufferFromByteArrayFactory,
+ * but doesn't perform as fast.
+ * When operating on a big dictionary, this class is preferred.
+ */
+ public static final class DictionaryBufferFromReadOnlyByteBufferFactory
+ implements DictionaryBufferFactory {
+ @Override
+ public DictBuffer getDictionaryBuffer(final File file)
+ throws FileNotFoundException, IOException {
+ FileInputStream inStream = null;
+ ByteBuffer buffer = null;
+ try {
+ inStream = new FileInputStream(file);
+ buffer = inStream.getChannel().map(FileChannel.MapMode.READ_ONLY,
+ 0, file.length());
+ } finally {
+ if (inStream != null) {
+ inStream.close();
+ }
+ }
+ if (buffer != null) {
+ return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Creates DictionaryBuffer using a byte array
+ *
+ * This class performs faster than other classes, but consumes more memory.
+ * When operating on a small dictionary, this class is preferred.
+ */
+ public static final class DictionaryBufferFromByteArrayFactory
+ implements DictionaryBufferFactory {
+ @Override
+ public DictBuffer getDictionaryBuffer(final File file)
+ throws FileNotFoundException, IOException {
+ FileInputStream inStream = null;
+ try {
+ inStream = new FileInputStream(file);
+ final byte[] array = new byte[(int) file.length()];
+ inStream.read(array);
+ return new ByteArrayDictBuffer(array);
+ } finally {
+ if (inStream != null) {
+ inStream.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates DictionaryBuffer using a writable ByteBuffer and a RandomAccessFile.
+ *
+ * This class doesn't perform as fast as other classes,
+ * but this class is the only option available for destructive operations (insert or delete)
+ * on a dictionary.
+ */
+ @UsedForTesting
+ public static final class DictionaryBufferFromWritableByteBufferFactory
+ implements DictionaryBufferFactory {
+ @Override
+ public DictBuffer getDictionaryBuffer(final File file)
+ throws FileNotFoundException, IOException {
+ RandomAccessFile raFile = null;
+ ByteBuffer buffer = null;
+ try {
+ raFile = new RandomAccessFile(file, "rw");
+ buffer = raFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length());
+ } finally {
+ if (raFile != null) {
+ raFile.close();
+ }
+ }
+ if (buffer != null) {
+ return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer);
+ }
+ return null;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
new file mode 100644
index 000000000..89c982e7b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
@@ -0,0 +1,29 @@
+/*
+ * 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.FormatSpec.FormatOptions;
+
+import java.io.IOException;
+
+/**
+ * An interface of binary dictionary encoder.
+ */
+public interface DictEncoder {
+ public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
+ 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
new file mode 100644
index 000000000..bf3d19101
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -0,0 +1,502 @@
+/*
+ * 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.Constants;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * The utility class to help dynamic updates on the binary dictionary.
+ *
+ * All the methods in this class are static.
+ */
+@UsedForTesting
+public final class DynamicBinaryDictIOUtils {
+ private static final boolean DBG = false;
+ private static final int MAX_JUMPS = 10000;
+
+ private DynamicBinaryDictIOUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private 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 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,
+ final int ptNodeOriginAddress, final int newParentAddress,
+ final FormatOptions formatOptions) {
+ final int originalPosition = dictBuffer.position();
+ dictBuffer.position(ptNodeOriginAddress);
+ if (!formatOptions.mSupportsDynamicUpdate) {
+ throw new RuntimeException("this file format does not support parent addresses");
+ }
+ final int flags = dictBuffer.readUnsignedByte();
+ if (BinaryDictIOUtils.isMovedPtNode(flags, formatOptions)) {
+ // If the node is moved, the parent address is stored in the destination node.
+ // We are guaranteed to process the destination node later, so there is no need to
+ // update anything here.
+ dictBuffer.position(originalPosition);
+ return;
+ }
+ if (DBG) {
+ MakedictLog.d("update parent address flags=" + flags + ", " + ptNodeOriginAddress);
+ }
+ final int parentOffset = newParentAddress - ptNodeOriginAddress;
+ BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, parentOffset);
+ dictBuffer.position(originalPosition);
+ }
+
+ /**
+ * Update parent addresses in a node array stored at ptNodeOriginAddress.
+ *
+ * @param dictBuffer the DictBuffer 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,
+ final int ptNodeOriginAddress, final int newParentAddress,
+ final FormatOptions formatOptions) {
+ final int originalPosition = dictBuffer.position();
+ dictBuffer.position(ptNodeOriginAddress);
+ do {
+ final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ for (int i = 0; i < count; ++i) {
+ updateParentAddress(dictBuffer, dictBuffer.position(), newParentAddress,
+ formatOptions);
+ BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions);
+ }
+ final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
+ dictBuffer.position(forwardLinkAddress);
+ } while (formatOptions.mSupportsDynamicUpdate
+ && dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+ dictBuffer.position(originalPosition);
+ }
+
+ /**
+ * Update a children address in a PtNode that is addressed by ptNodeOriginAddress.
+ *
+ * @param dictBuffer the DictBuffer 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,
+ final int ptNodeOriginAddress, final int newChildrenAddress,
+ final FormatOptions formatOptions) {
+ final int originalPosition = dictBuffer.position();
+ dictBuffer.position(ptNodeOriginAddress);
+ final int flags = dictBuffer.readUnsignedByte();
+ final int parentAddress = 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
+ ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - dictBuffer.position();
+ BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, childrenOffset);
+ dictBuffer.position(originalPosition);
+ }
+
+ /**
+ * 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 int nodeArrayOriginAddress, final int oldNodeAddress,
+ final FormatOptions formatOptions) throws IOException {
+ updateParentAddress(dictBuffer, 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);
+ size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info });
+ return size;
+ }
+
+ @SuppressWarnings("unused")
+ private static void updateForwardLink(final DictBuffer dictBuffer,
+ final int nodeArrayOriginAddress, final int newNodeArrayAddress,
+ final FormatOptions formatOptions) {
+ dictBuffer.position(nodeArrayOriginAddress);
+ int jumpCount = 0;
+ while (jumpCount++ < MAX_JUMPS) {
+ final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ for (int i = 0; i < count; ++i) {
+ BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions);
+ }
+ final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
+ if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+ dictBuffer.position(dictBuffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
+ BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeArrayAddress);
+ return;
+ }
+ dictBuffer.position(forwardLinkAddress);
+ }
+ if (DBG && jumpCount >= MAX_JUMPS) {
+ throw new RuntimeException("too many jumps, probably a bug.");
+ }
+ }
+
+ /**
+ * Move a PtNode that is referred to by oldPtNodeOrigin to the tail of the file, and set the
+ * children address to the byte after the PtNode.
+ *
+ * @param fileEndAddress the address of the tail of the file.
+ * @param codePoints the characters to put inside the PtNode.
+ * @param length how many code points to read from codePoints.
+ * @param flags the flags for this PtNode.
+ * @param frequency the frequency of this terminal.
+ * @param parentAddress the address of the parent PtNode of this PtNode.
+ * @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 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.
+ * @return the size written, in bytes.
+ * @throws IOException if the file can't be accessed
+ */
+ private static int movePtNode(final int fileEndAddress, final int[] codePoints,
+ 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 int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException {
+ int size = 0;
+ final int newPtNodeOrigin = fileEndAddress + 1;
+ final int[] writtenCharacters = Arrays.copyOfRange(codePoints, 0, length);
+ final PtNodeInfo tmpInfo = new PtNodeInfo(newPtNodeOrigin, -1 /* endAddress */,
+ flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
+ shortcutTargets, bigrams);
+ size = BinaryDictIOUtils.computePtNodeSize(tmpInfo, formatOptions);
+ final PtNodeInfo newInfo = new PtNodeInfo(newPtNodeOrigin, newPtNodeOrigin + size,
+ flags, writtenCharacters, frequency, parentAddress,
+ fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
+ bigrams);
+ movePtNode(destination, dictBuffer, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin,
+ formatOptions);
+ return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ }
+
+ /**
+ * Insert a word into a binary dictionary.
+ *
+ * @param dictDecoder the dict decoder.
+ * @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.
+ * @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.
+ * @throws IOException if the file can't be accessed.
+ * @throws UnsupportedFormatException if the existing dictionary is in an unexpected format.
+ */
+ // TODO: Support batch insertion.
+ // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
+ @UsedForTesting
+ public static void insertWord(final Ver3DictDecoder dictDecoder,
+ 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();
+ if (bigramStrings != null) {
+ for (final WeightedString bigram : bigramStrings) {
+ int position = dictDecoder.getTerminalPosition(bigram.mWord);
+ if (position == FormatSpec.NOT_VALID_WORD) {
+ // TODO: figure out what is the correct thing to do here.
+ } else {
+ bigrams.add(new PendingAttribute(bigram.mFrequency, position));
+ }
+ }
+ }
+
+ final boolean isTerminal = true;
+ final boolean hasBigrams = !bigrams.isEmpty();
+ final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
+
+ // find the insert position of the word.
+ if (dictBuffer.position() != 0) dictBuffer.position(0);
+ final FileHeader fileHeader = dictDecoder.readHeader();
+
+ int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position();
+ final int[] codePoints = FusionDictionary.getCodePoints(word);
+ final int wordLen = codePoints.length;
+
+ for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
+ if (wordPos >= wordLen) break;
+ nodeOriginAddress = dictBuffer.position();
+ int nodeParentAddress = -1;
+ final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ boolean foundNextNode = false;
+
+ for (int i = 0; i < ptNodeCount; ++i) {
+ address = dictBuffer.position();
+ final PtNodeInfo currentInfo = dictDecoder.readPtNode(address,
+ fileHeader.mFormatOptions);
+ final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags,
+ fileHeader.mFormatOptions);
+ if (isMovedNode) continue;
+ nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
+ ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
+ boolean matched = true;
+ for (int p = 0; p < currentInfo.mCharacters.length; ++p) {
+ if (wordPos + p >= wordLen) {
+ /*
+ * splitting
+ * before
+ * abcd - ef
+ *
+ * insert "abc"
+ *
+ * after
+ * abc - d - ef
+ */
+ final int newNodeAddress = dictBuffer.limit();
+ final int flags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1,
+ isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
+ false /* isBlackListEntry */, fileHeader.mFormatOptions);
+ int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags,
+ frequency, nodeParentAddress, shortcuts, bigrams, destination,
+ dictBuffer, 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,
+ newNodeAddress + written + 1, fileHeader.mFormatOptions);
+ }
+ final PtNodeInfo newInfo2 = new PtNodeInfo(
+ newNodeAddress + written + 1, -1 /* endAddress */,
+ currentInfo.mFlags, characters2, currentInfo.mFrequency,
+ newNodeAddress + 1, currentInfo.mChildrenAddress,
+ currentInfo.mShortcutTargets, currentInfo.mBigrams);
+ BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo2 });
+ return;
+ } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) {
+ if (p > 0) {
+ /*
+ * splitting
+ * before
+ * ab - cd
+ *
+ * insert "ac"
+ *
+ * after
+ * a - b - cd
+ * |
+ * - c
+ */
+
+ final int newNodeAddress = dictBuffer.limit();
+ final int childrenAddress = currentInfo.mChildrenAddress;
+
+ // move prefix
+ final int prefixFlags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1,
+ false /* isTerminal */, 0 /* childrenAddressSize*/,
+ false /* hasShortcut */, false /* hasBigrams */,
+ false /* isNotAWord */, false /* isBlackListEntry */,
+ fileHeader.mFormatOptions);
+ int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p,
+ prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
+ destination, dictBuffer, 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,
+ newNodeAddress + written + 1, fileHeader.mFormatOptions);
+ }
+ final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags(
+ suffixCharacters.length > 1,
+ (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0,
+ 0 /* childrenAddressSize */,
+ (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
+ != 0,
+ (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
+ isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
+ final PtNodeInfo suffixInfo = new PtNodeInfo(
+ newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
+ suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
+ currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
+ currentInfo.mBigrams);
+ written += BinaryDictIOUtils.computePtNodeSize(suffixInfo,
+ fileHeader.mFormatOptions) + 1;
+
+ final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
+ codePoints.length);
+ final int flags = BinaryDictEncoderUtils.makePtNodeFlags(
+ newCharacters.length > 1, isTerminal,
+ 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+ isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
+ final PtNodeInfo newInfo = new PtNodeInfo(
+ newNodeAddress + written, -1 /* endAddress */, flags,
+ newCharacters, frequency, newNodeAddress + 1,
+ FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
+ BinaryDictIOUtils.writeNodes(destination,
+ new PtNodeInfo[] { suffixInfo, newInfo });
+ return;
+ }
+ matched = false;
+ break;
+ }
+ }
+
+ if (matched) {
+ if (wordPos + currentInfo.mCharacters.length == wordLen) {
+ // the word exists in the dictionary.
+ // only update the PtNode.
+ final int newNodeAddress = dictBuffer.limit();
+ final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
+ final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars,
+ isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+ isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
+ final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1,
+ -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
+ nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
+ bigrams);
+ movePtNode(destination, dictBuffer, newInfo, nodeOriginAddress, address,
+ fileHeader.mFormatOptions);
+ return;
+ }
+ wordPos += currentInfo.mCharacters.length;
+ if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
+ /*
+ * found the prefix of the word.
+ * make new PtNode and link to the PtNode from this PtNode.
+ *
+ * before
+ * ab - cd
+ *
+ * insert "abcde"
+ *
+ * after
+ * ab - cd - e
+ */
+ final int newNodeArrayAddress = dictBuffer.limit();
+ updateChildrenAddress(dictBuffer, address, newNodeArrayAddress,
+ fileHeader.mFormatOptions);
+ final int newNodeAddress = newNodeArrayAddress + 1;
+ final boolean hasMultipleChars = (wordLen - wordPos) > 1;
+ final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars,
+ isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+ isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
+ final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
+ final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress, -1, flags,
+ characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
+ shortcuts, bigrams);
+ BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo });
+ return;
+ }
+ dictBuffer.position(currentInfo.mChildrenAddress);
+ foundNextNode = true;
+ break;
+ }
+ }
+
+ if (foundNextNode) continue;
+
+ // reached the end of the array.
+ final int linkAddressPosition = dictBuffer.position();
+ int nextLink = dictBuffer.readUnsignedInt24();
+ if ((nextLink & FormatSpec.MSB24) != 0) {
+ nextLink = -(nextLink & FormatSpec.SINT24_MAX);
+ }
+ if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+ /*
+ * expand this node.
+ *
+ * before
+ * ab - cd
+ *
+ * insert "abef"
+ *
+ * after
+ * ab - cd
+ * |
+ * - ef
+ */
+
+ // change the forward link address.
+ final int newNodeAddress = dictBuffer.limit();
+ dictBuffer.position(linkAddressPosition);
+ BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeAddress);
+
+ final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
+ final int flags = BinaryDictEncoderUtils.makePtNodeFlags(characters.length > 1,
+ isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
+ isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
+ final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1,
+ -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
+ FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
+ BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[]{ newInfo });
+ return;
+ } else {
+ depth--;
+ dictBuffer.position(nextLink);
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 46266aa50..b8ef57696 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -26,21 +26,55 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions
public final class FormatSpec {
/*
- * Array of Node(FusionDictionary.Node) layout is as follows:
+ * File header layout is as follows:
*
- * g |
- * r | the number of groups, 1 or 2 bytes.
- * o | 1 byte = bbbbbbbb match
- * u | case 1xxxxxxx => xxxxxxx << 8 + next byte
- * p | otherwise => bbbbbbbb
- * c |
- * ount
+ * v |
+ * e | MAGIC_NUMBER + version of the file format, 2 bytes.
+ * r |
+ * sion
*
- * g |
- * r | sequence of groups,
- * o | the layout of each group is described below.
- * u |
- * ps
+ * o |
+ * p | not used 4 bits
+ * t | has bigrams ? 1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG
+ * i | FRENCH_LIGATURE_PROCESSING_FLAG
+ * o | supports dynamic updates ? 1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE
+ * n | GERMAN_UMLAUT_PROCESSING_FLAG
+ * f |
+ * lags
+ *
+ * h |
+ * e | size of the file header, 4bytes
+ * a | including the size of the magic number, the option flags and the header size
+ * d |
+ * ersize
+ *
+ * | attributes list
+ *
+ * attributes list is:
+ * <key> = | string of characters at the char format described below, with the terminator used
+ * | to signal the end of the string.
+ * <value> = | string of characters at the char format described below, with the terminator used
+ * | to signal the end of the string.
+ * if the size of already read < headersize, goto key.
+ *
+ */
+
+ /*
+ * Node array (FusionDictionary.PtNodeArray) layout is as follows:
+ *
+ * n |
+ * o | the number of PtNodes, 1 or 2 bytes.
+ * d | 1 byte = bbbbbbbb match
+ * e | case 1xxxxxxx => xxxxxxx << 8 + next byte
+ * c | otherwise => bbbbbbbb
+ * o |
+ * unt
+ *
+ * n |
+ * o | sequence of PtNodes,
+ * d | the layout of each PtNode is described below.
+ * e |
+ * s
*
* f |
* o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
@@ -52,19 +86,19 @@ public final class FormatSpec {
* linkaddress
*/
- /* Node(CharGroup) layout is as follows:
+ /* Node (FusionDictionary.PtNode) layout is as follows:
* | IF !SUPPORTS_DYNAMIC_UPDATE
- * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE
- * | 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
- * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
- * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
- * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+ * | addressType xx : mask with MASK_CHILDREN_ADDRESS_TYPE
+ * | 2 bits, 00 = no children : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS
+ * f | 01 = 1 byte : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE
+ * l | 10 = 2 bytes : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES
+ * a | 11 = 3 bytes : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES
* g | ELSE
- * s | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED
- * | This must be the same as FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
- * | 01 = yes : FLAG_IS_MOVED
+ * s | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED
+ * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES
+ * | 01 = yes : FLAG_IS_MOVED
* | the new address is stored in the same place as the parent address
- * | is deleted? 10 = yes : FLAG_IS_DELETED
+ * | is deleted? 10 = yes : FLAG_IS_DELETED
* | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
* | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
* | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS
@@ -82,7 +116,7 @@ public final class FormatSpec {
* ddress
*
* c | IF FLAG_HAS_MULTIPLE_CHARS
- * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers
* a | end 1 byte, = 0
* r | ELSE
* s | char 1 or 3 bytes
@@ -93,17 +127,22 @@ public final class FormatSpec {
* e | frequency 1 byte
* q |
*
- * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
- * h | // nothing
- * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
- * l | children address, 1 byte
- * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
- * r | children address, 2 bytes
- * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
- * n | children address, 3 bytes
- * A | END
- * d
- * dress
+ * c | IF SUPPORTS_DYNAMIC_UPDATE
+ * h | children address, 3 bytes
+ * i | 1 byte = bbbbbbbb match
+ * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte)
+ * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte
+ * r | ELSIF 00 = FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS == addressType
+ * e | // nothing
+ * n | ELSIF 01 = FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE == addressType
+ * A | children address, 1 byte
+ * d | ELSIF 10 = FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES == addressType
+ * d | children address, 2 bytes
+ * r | ELSE // 11 = FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = addressType
+ * e | children address, 3 bytes
+ * s | END
+ * s
+ * ress
*
* | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
* | shortcut string list
@@ -122,41 +161,39 @@ public final class FormatSpec {
* characters which should never happen anyway (and still work, but take 3 bytes).
*
* bigram address list is:
- * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
- * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE
* | 1 = must take -address, 0 = must take +address
- * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
- * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
- * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
- * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
- * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
- * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
- * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+ * | xx : mask with MASK_BIGRAM_ATTR_ADDRESS_TYPE
+ * | addressFormat = 2 bits, 00 = unused : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE
+ * | 01 = 1 byte : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE
+ * | 10 = 2 bytes : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES
+ * | 11 = 3 bytes : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES
+ * | 4 bits : frequency : mask with FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY
+ * <address> | IF (01 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE == addressFormat)
* | read 1 byte, add top 4 bits
- * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | ELSIF (10 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES == addressFormat)
* | read 2 bytes, add top 4 bits
- * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+ * | ELSE // 11 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES == addressFormat
* | read 3 bytes, add top 4 bits
* | END
- * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
- * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is
+ * | if (FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE) then address = -address
+ * if (FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) goto bigram_and_shortcut_address_list_is
*
* shortcut string list is:
- * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
- * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * <byte size> = PTNODE_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT
* | reserved = 3 bits, must be 0
- * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * | 4 bits : frequency : mask with FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY
* <shortcut> = | string of characters at the char format described above, with the terminator
* | used to signal the end of the string.
- * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags
+ * if (FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT goto flags
*/
- static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
- public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
- static final int MINIMUM_SUPPORTED_VERSION = 1;
+ public static final int MAGIC_NUMBER = 0x9BC13AFE;
+ static final int MINIMUM_SUPPORTED_VERSION = 2;
static final int MAXIMUM_SUPPORTED_VERSION = 3;
static final int NOT_A_VERSION_NUMBER = -1;
- static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3;
// These options need to be the same numeric values as the one in the native reading code.
@@ -174,11 +211,11 @@ public final class FormatSpec {
static final int FORWARD_LINK_ADDRESS_SIZE = 3;
// These flags are used only in the static dictionary.
- static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
- static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
- static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
- static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
- static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+ static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS = 0x00;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE = 0x40;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES = 0x80;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = 0xC0;
static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
@@ -195,32 +232,32 @@ public final class FormatSpec {
static final int FLAG_IS_NOT_MOVED = 0x80 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE;
static final int FLAG_IS_DELETED = 0x80;
- static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
- static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
- static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
- static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+ static final int FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT = 0x80;
+ static final int FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE = 0x40;
+ static final int MASK_BIGRAM_ATTR_ADDRESS_TYPE = 0x30;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE = 0x10;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES = 0x20;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES = 0x30;
+ static final int FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY = 0x0F;
- static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+ static final int PTNODE_CHARACTERS_TERMINATOR = 0x1F;
- static final int GROUP_TERMINATOR_SIZE = 1;
- static final int GROUP_FLAGS_SIZE = 1;
- static final int GROUP_FREQUENCY_SIZE = 1;
- static final int GROUP_MAX_ADDRESS_SIZE = 3;
- static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
- static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
- static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
+ static final int PTNODE_TERMINATOR_SIZE = 1;
+ static final int PTNODE_FLAGS_SIZE = 1;
+ static final int PTNODE_FREQUENCY_SIZE = 1;
+ static final int PTNODE_MAX_ADDRESS_SIZE = 3;
+ static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1;
+ static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+ static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2;
static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
static final int NO_PARENT_ADDRESS = 0;
static final int NO_FORWARD_LINK_ADDRESS = 0;
static final int INVALID_CHARACTER = -1;
- static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
- static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
- static final int MAX_BIGRAMS_IN_A_GROUP = 10000;
+ static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127
+ static final int MAX_PTNODES_IN_A_PT_NODE_ARRAY = 0x7FFF; // 32767
+ static final int MAX_BIGRAMS_IN_A_PTNODE = 10000;
static final int MAX_TERMINAL_FREQUENCY = 255;
static final int MAX_BIGRAM_FREQUENCY = 15;
@@ -231,6 +268,13 @@ public final class FormatSpec {
static final int NOT_VALID_WORD = -99;
static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3;
+ static final int UINT8_MAX = 0xFF;
+ static final int UINT16_MAX = 0xFFFF;
+ static final int UINT24_MAX = 0xFFFFFF;
+ static final int SINT24_MAX = 0x7FFFFF;
+ static final int MSB8 = 0x80;
+ static final int MSB24 = 0x800000;
+
/**
* Options about file format.
*/
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 118dc22b8..3e685a3d7 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -37,15 +37,15 @@ public final class FusionDictionary implements Iterable<Word> {
private static int CHARACTER_NOT_FOUND_INDEX = -1;
/**
- * A node of the dictionary, containing several CharGroups.
+ * A node array of the dictionary, containing several PtNodes.
*
- * A node is but an ordered array of CharGroups, which essentially contain all the
+ * A PtNodeArray is but an ordered array of PtNodes, which essentially contain all the
* real information.
* This class also contains fields to cache size and address, to help with binary
* generation.
*/
- public static final class Node {
- ArrayList<CharGroup> mData;
+ public static final class PtNodeArray {
+ ArrayList<PtNode> mData;
// To help with binary generation
int mCachedSize = Integer.MIN_VALUE;
// mCachedAddressBefore/AfterUpdate are helpers for binary dictionary generation. They
@@ -57,10 +57,10 @@ public final class FusionDictionary implements Iterable<Word> {
int mCachedAddressAfterUpdate = Integer.MIN_VALUE;
int mCachedParentAddress = 0;
- public Node() {
- mData = new ArrayList<CharGroup>();
+ public PtNodeArray() {
+ mData = new ArrayList<PtNode>();
}
- public Node(ArrayList<CharGroup> data) {
+ public PtNodeArray(ArrayList<PtNode> data) {
mData = data;
}
}
@@ -93,24 +93,25 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * A group of characters, with a frequency, shortcut targets, bigrams, and children.
+ * PtNode is a group of characters, with a frequency, shortcut targets, bigrams, and children
+ * (Pt means Patricia Trie).
*
- * This is the central class of the in-memory representation. A CharGroup is what can
+ * This is the central class of the in-memory representation. A PtNode is what can
* be seen as a traditional "trie node", except it can hold several characters at the
- * same time. A CharGroup essentially represents one or several characters in the middle
- * of the trie trie; as such, it can be a terminal, and it can have children.
- * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+ * same time. A PtNode essentially represents one or several characters in the middle
+ * of the trie tree; as such, it can be a terminal, and it can have children.
+ * In this in-memory representation, whether the PtNode is a terminal or not is represented
* in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other
* value is the frequency of this terminal. A terminal may have non-null shortcuts and/or
* bigrams, but a non-terminal may not. Moreover, children, if present, are null.
*/
- public static final class CharGroup {
+ public static final class PtNode {
public static final int NOT_A_TERMINAL = -1;
final int mChars[];
ArrayList<WeightedString> mShortcutTargets;
ArrayList<WeightedString> mBigrams;
int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
- Node mChildren;
+ PtNodeArray mChildren;
boolean mIsNotAWord; // Only a shortcut
boolean mIsBlacklistEntry;
// mCachedSize and mCachedAddressBefore/AfterUpdate are helpers for binary dictionary
@@ -119,11 +120,11 @@ public final class FusionDictionary implements Iterable<Word> {
// same time. Updating will update the AfterUpdate value, and the code will move them
// to BeforeUpdate before the next update pass.
// The update process does not need two versions of mCachedSize.
- int mCachedSize; // The size, in bytes, of this char group.
- int mCachedAddressBeforeUpdate; // The address of this char group (before update)
- int mCachedAddressAfterUpdate; // The address of this char group (after update)
+ int mCachedSize; // The size, in bytes, of this PtNode.
+ int mCachedAddressBeforeUpdate; // The address of this PtNode (before update)
+ int mCachedAddressAfterUpdate; // The address of this PtNode (after update)
- public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
final ArrayList<WeightedString> bigrams, final int frequency,
final boolean isNotAWord, final boolean isBlacklistEntry) {
mChars = chars;
@@ -135,9 +136,10 @@ public final class FusionDictionary implements Iterable<Word> {
mIsBlacklistEntry = isBlacklistEntry;
}
- public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
final ArrayList<WeightedString> bigrams, final int frequency,
- final boolean isNotAWord, final boolean isBlacklistEntry, final Node children) {
+ final boolean isNotAWord, final boolean isBlacklistEntry,
+ final PtNodeArray children) {
mChars = chars;
mFrequency = frequency;
mShortcutTargets = shortcutTargets;
@@ -147,9 +149,9 @@ public final class FusionDictionary implements Iterable<Word> {
mIsBlacklistEntry = isBlacklistEntry;
}
- public void addChild(CharGroup n) {
+ public void addChild(PtNode n) {
if (null == mChildren) {
- mChildren = new Node();
+ mChildren = new PtNodeArray();
}
mChildren.mData.add(n);
}
@@ -244,7 +246,7 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Updates the CharGroup with the given properties. Adds the shortcut and bigram lists to
+ * Updates the PtNode with the given properties. Adds the shortcut and bigram lists to
* the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only
* updated if they are higher than the existing ones.
*/
@@ -344,10 +346,10 @@ public final class FusionDictionary implements Iterable<Word> {
}
public final DictionaryOptions mOptions;
- public final Node mRoot;
+ public final PtNodeArray mRootNodeArray;
- public FusionDictionary(final Node root, final DictionaryOptions options) {
- mRoot = root;
+ public FusionDictionary(final PtNodeArray rootNodeArray, final DictionaryOptions options) {
+ mRootNodeArray = rootNodeArray;
mOptions = options;
}
@@ -406,13 +408,13 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Sanity check for a node.
+ * Sanity check for a PtNode array.
*
- * This method checks that all CharGroups in a node are ordered as expected.
+ * This method checks that all PtNodes in a node array are ordered as expected.
* If they are, nothing happens. If they aren't, an exception is thrown.
*/
- private void checkStack(Node node) {
- ArrayList<CharGroup> stack = node.mData;
+ private void checkStack(PtNodeArray ptNodeArray) {
+ ArrayList<PtNode> stack = ptNodeArray.mData;
int lastValue = -1;
for (int i = 0; i < stack.size(); ++i) {
int currentValue = stack.get(i).mChars[0];
@@ -431,18 +433,18 @@ public final class FusionDictionary implements Iterable<Word> {
* @param frequency the bigram frequency
*/
public void setBigram(final String word1, final String word2, final int frequency) {
- CharGroup charGroup = findWordInTree(mRoot, word1);
- if (charGroup != null) {
- final CharGroup charGroup2 = findWordInTree(mRoot, word2);
- if (charGroup2 == null) {
+ PtNode ptNode = findWordInTree(mRootNodeArray, word1);
+ if (ptNode != null) {
+ final PtNode ptNode2 = findWordInTree(mRootNodeArray, word2);
+ if (ptNode2 == null) {
add(getCodePoints(word2), 0, null, false /* isNotAWord */,
false /* isBlacklistEntry */);
- // The chargroup for the first word may have moved by the above insertion,
+ // The PtNode for the first word may have moved by the above insertion,
// if word1 and word2 share a common stem that happens not to have been
- // a cutting point until now. In this case, we need to refresh charGroup.
- charGroup = findWordInTree(mRoot, word1);
+ // a cutting point until now. In this case, we need to refresh ptNode.
+ ptNode = findWordInTree(mRootNodeArray, word1);
}
- charGroup.addBigram(word2, frequency);
+ ptNode.addBigram(word2, frequency);
} else {
throw new RuntimeException("First word of bigram not found");
}
@@ -469,92 +471,91 @@ public final class FusionDictionary implements Iterable<Word> {
return;
}
- Node currentNode = mRoot;
+ PtNodeArray currentNodeArray = mRootNodeArray;
int charIndex = 0;
- CharGroup currentGroup = null;
+ PtNode currentPtNode = null;
int differentCharIndex = 0; // Set by the loop to the index of the char that differs
- int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
+ int nodeIndex = findIndexOfChar(mRootNodeArray, word[charIndex]);
while (CHARACTER_NOT_FOUND_INDEX != nodeIndex) {
- currentGroup = currentNode.mData.get(nodeIndex);
- differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex);
+ currentPtNode = currentNodeArray.mData.get(nodeIndex);
+ differentCharIndex = compareCharArrays(currentPtNode.mChars, word, charIndex);
if (ARRAYS_ARE_EQUAL != differentCharIndex
- && differentCharIndex < currentGroup.mChars.length) break;
- if (null == currentGroup.mChildren) break;
- charIndex += currentGroup.mChars.length;
+ && differentCharIndex < currentPtNode.mChars.length) break;
+ if (null == currentPtNode.mChildren) break;
+ charIndex += currentPtNode.mChars.length;
if (charIndex >= word.length) break;
- currentNode = currentGroup.mChildren;
- nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+ currentNodeArray = currentPtNode.mChildren;
+ nodeIndex = findIndexOfChar(currentNodeArray, word[charIndex]);
}
if (CHARACTER_NOT_FOUND_INDEX == nodeIndex) {
// No node at this point to accept the word. Create one.
- final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
- final CharGroup newGroup = new CharGroup(
- Arrays.copyOfRange(word, charIndex, word.length),
+ final int insertionIndex = findInsertionIndex(currentNodeArray, word[charIndex]);
+ final PtNode newPtNode = new PtNode(Arrays.copyOfRange(word, charIndex, word.length),
shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry);
- currentNode.mData.add(insertionIndex, newGroup);
- if (DBG) checkStack(currentNode);
+ currentNodeArray.mData.add(insertionIndex, newPtNode);
+ if (DBG) checkStack(currentNodeArray);
} else {
// There is a word with a common prefix.
- if (differentCharIndex == currentGroup.mChars.length) {
+ if (differentCharIndex == currentPtNode.mChars.length) {
if (charIndex + differentCharIndex >= word.length) {
// The new word is a prefix of an existing word, but the node on which it
- // should end already exists as is. Since the old CharNode was not a terminal,
+ // should end already exists as is. Since the old PtNode was not a terminal,
// make it one by filling in its frequency and other attributes
- currentGroup.update(frequency, shortcutTargets, null, isNotAWord,
+ currentPtNode.update(frequency, shortcutTargets, null, isNotAWord,
isBlacklistEntry);
} else {
// The new word matches the full old word and extends past it.
// We only have to create a new node and add it to the end of this.
- final CharGroup newNode = new CharGroup(
+ final PtNode newNode = new PtNode(
Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
shortcutTargets, null /* bigrams */, frequency, isNotAWord,
isBlacklistEntry);
- currentGroup.mChildren = new Node();
- currentGroup.mChildren.mData.add(newNode);
+ currentPtNode.mChildren = new PtNodeArray();
+ currentPtNode.mChildren.mData.add(newNode);
}
} else {
if (0 == differentCharIndex) {
// Exact same word. Update the frequency if higher. This will also add the
// new shortcuts to the existing shortcut list if it already exists.
- currentGroup.update(frequency, shortcutTargets, null,
- currentGroup.mIsNotAWord && isNotAWord,
- currentGroup.mIsBlacklistEntry || isBlacklistEntry);
+ currentPtNode.update(frequency, shortcutTargets, null,
+ currentPtNode.mIsNotAWord && isNotAWord,
+ currentPtNode.mIsBlacklistEntry || isBlacklistEntry);
} else {
// Partial prefix match only. We have to replace the current node with a node
// containing the current prefix and create two new ones for the tails.
- Node newChildren = new Node();
- final CharGroup newOldWord = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
- currentGroup.mChars.length), currentGroup.mShortcutTargets,
- currentGroup.mBigrams, currentGroup.mFrequency,
- currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry,
- currentGroup.mChildren);
+ PtNodeArray newChildren = new PtNodeArray();
+ final PtNode newOldWord = new PtNode(
+ Arrays.copyOfRange(currentPtNode.mChars, differentCharIndex,
+ currentPtNode.mChars.length), currentPtNode.mShortcutTargets,
+ currentPtNode.mBigrams, currentPtNode.mFrequency,
+ currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry,
+ currentPtNode.mChildren);
newChildren.mData.add(newOldWord);
- final CharGroup newParent;
+ final PtNode newParent;
if (charIndex + differentCharIndex >= word.length) {
- newParent = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ newParent = new PtNode(
+ Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex),
shortcutTargets, null /* bigrams */, frequency,
isNotAWord, isBlacklistEntry, newChildren);
} else {
- newParent = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ newParent = new PtNode(
+ Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex),
null /* shortcutTargets */, null /* bigrams */, -1,
false /* isNotAWord */, false /* isBlacklistEntry */, newChildren);
- final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word,
+ final PtNode newWord = new PtNode(Arrays.copyOfRange(word,
charIndex + differentCharIndex, word.length),
shortcutTargets, null /* bigrams */, frequency,
isNotAWord, isBlacklistEntry);
final int addIndex = word[charIndex + differentCharIndex]
- > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+ > currentPtNode.mChars[differentCharIndex] ? 1 : 0;
newChildren.mData.add(addIndex, newWord);
}
- currentNode.mData.set(nodeIndex, newParent);
+ currentNodeArray.mData.set(nodeIndex, newParent);
}
- if (DBG) checkStack(currentNode);
+ if (DBG) checkStack(currentNodeArray);
}
}
}
@@ -576,7 +577,7 @@ public final class FusionDictionary implements Iterable<Word> {
* @param dstOffset the offset in the right-hand side string.
* @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't.
*/
- private static int compareArrays(final int[] src, final int[] dst, int dstOffset) {
+ private static int compareCharArrays(final int[] src, final int[] dst, int dstOffset) {
// We do NOT test the first char, because we come from a method that already
// tested it.
for (int i = 1; i < src.length; ++i) {
@@ -588,43 +589,43 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Helper class that compares and sorts two chargroups according to their
+ * Helper class that compares and sorts two PtNodes according to their
* first element only. I repeat: ONLY the first element is considered, the rest
* is ignored.
* This comparator imposes orderings that are inconsistent with equals.
*/
- static private final class CharGroupComparator implements java.util.Comparator<CharGroup> {
+ static private final class PtNodeComparator implements java.util.Comparator<PtNode> {
@Override
- public int compare(CharGroup c1, CharGroup c2) {
- if (c1.mChars[0] == c2.mChars[0]) return 0;
- return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+ public int compare(PtNode p1, PtNode p2) {
+ if (p1.mChars[0] == p2.mChars[0]) return 0;
+ return p1.mChars[0] < p2.mChars[0] ? -1 : 1;
}
}
- final static private CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+ final static private PtNodeComparator PTNODE_COMPARATOR = new PtNodeComparator();
/**
- * Finds the insertion index of a character within a node.
+ * Finds the insertion index of a character within a node array.
*/
- private static int findInsertionIndex(final Node node, int character) {
- final ArrayList<CharGroup> data = node.mData;
- final CharGroup reference = new CharGroup(new int[] { character },
+ private static int findInsertionIndex(final PtNodeArray nodeArray, int character) {
+ final ArrayList<PtNode> data = nodeArray.mData;
+ final PtNode reference = new PtNode(new int[] { character },
null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */,
false /* isBlacklistEntry */);
- int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
+ int result = Collections.binarySearch(data, reference, PTNODE_COMPARATOR);
return result >= 0 ? result : -result - 1;
}
/**
- * Find the index of a char in a node, if it exists.
+ * Find the index of a char in a node array, if it exists.
*
- * @param node the node to search in.
+ * @param nodeArray the node array to search in.
* @param character the character to search for.
* @return the position of the character if it's there, or CHARACTER_NOT_FOUND_INDEX = -1 else.
*/
- private static int findIndexOfChar(final Node node, int character) {
- final int insertionIndex = findInsertionIndex(node, character);
- if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND_INDEX;
- return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
+ private static int findIndexOfChar(final PtNodeArray nodeArray, int character) {
+ final int insertionIndex = findInsertionIndex(nodeArray, character);
+ if (nodeArray.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND_INDEX;
+ return character == nodeArray.mData.get(insertionIndex).mChars[0] ? insertionIndex
: CHARACTER_NOT_FOUND_INDEX;
}
@@ -632,35 +633,37 @@ public final class FusionDictionary implements Iterable<Word> {
* Helper method to find a word in a given branch.
*/
@SuppressWarnings("unused")
- public static CharGroup findWordInTree(Node node, final String string) {
+ public static PtNode findWordInTree(PtNodeArray nodeArray, final String string) {
int index = 0;
final StringBuilder checker = DBG ? new StringBuilder() : null;
final int[] codePoints = getCodePoints(string);
- CharGroup currentGroup;
+ PtNode currentPtNode;
do {
- int indexOfGroup = findIndexOfChar(node, codePoints[index]);
+ int indexOfGroup = findIndexOfChar(nodeArray, codePoints[index]);
if (CHARACTER_NOT_FOUND_INDEX == indexOfGroup) return null;
- currentGroup = node.mData.get(indexOfGroup);
+ currentPtNode = nodeArray.mData.get(indexOfGroup);
- if (codePoints.length - index < currentGroup.mChars.length) return null;
+ if (codePoints.length - index < currentPtNode.mChars.length) return null;
int newIndex = index;
- while (newIndex < codePoints.length && newIndex - index < currentGroup.mChars.length) {
- if (currentGroup.mChars[newIndex - index] != codePoints[newIndex]) return null;
+ while (newIndex < codePoints.length && newIndex - index < currentPtNode.mChars.length) {
+ if (currentPtNode.mChars[newIndex - index] != codePoints[newIndex]) return null;
newIndex++;
}
index = newIndex;
- if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
+ if (DBG) {
+ checker.append(new String(currentPtNode.mChars, 0, currentPtNode.mChars.length));
+ }
if (index < codePoints.length) {
- node = currentGroup.mChildren;
+ nodeArray = currentPtNode.mChildren;
}
- } while (null != node && index < codePoints.length);
+ } while (null != nodeArray && index < codePoints.length);
if (index < codePoints.length) return null;
- if (!currentGroup.isTerminal()) return null;
+ if (!currentPtNode.isTerminal()) return null;
if (DBG && !string.equals(checker.toString())) return null;
- return currentGroup;
+ return currentPtNode;
}
/**
@@ -670,22 +673,22 @@ public final class FusionDictionary implements Iterable<Word> {
if (null == s || "".equals(s)) {
throw new RuntimeException("Can't search for a null or empty string");
}
- return null != findWordInTree(mRoot, s);
+ return null != findWordInTree(mRootNodeArray, s);
}
/**
- * Recursively count the number of character groups in a given branch of the trie.
+ * Recursively count the number of PtNodes in a given branch of the trie.
*
- * @param node the parent node.
- * @return the number of char groups in all the branch under this node.
+ * @param nodeArray the parent node.
+ * @return the number of PtNodes in all the branch under this node.
*/
- public static int countCharGroups(final Node node) {
- final int nodeSize = node.mData.size();
+ public static int countPtNodes(final PtNodeArray nodeArray) {
+ final int nodeSize = nodeArray.mData.size();
int size = nodeSize;
for (int i = nodeSize - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mChildren)
- size += countCharGroups(group.mChildren);
+ PtNode ptNode = nodeArray.mData.get(i);
+ if (null != ptNode.mChildren)
+ size += countPtNodes(ptNode.mChildren);
}
return size;
}
@@ -693,15 +696,15 @@ public final class FusionDictionary implements Iterable<Word> {
/**
* Recursively count the number of nodes in a given branch of the trie.
*
- * @param node the node to count.
+ * @param nodeArray the node array to count.
* @return the number of nodes in this branch.
*/
- public static int countNodes(final Node node) {
+ public static int countNodeArrays(final PtNodeArray nodeArray) {
int size = 1;
- for (int i = node.mData.size() - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mChildren)
- size += countNodes(group.mChildren);
+ for (int i = nodeArray.mData.size() - 1; i >= 0; --i) {
+ PtNode ptNode = nodeArray.mData.get(i);
+ if (null != ptNode.mChildren)
+ size += countNodeArrays(ptNode.mChildren);
}
return size;
}
@@ -709,12 +712,12 @@ public final class FusionDictionary implements Iterable<Word> {
// Recursively find out whether there are any bigrams.
// This can be pretty expensive especially if there aren't any (we return as soon
// as we find one, so it's much cheaper if there are bigrams)
- private static boolean hasBigramsInternal(final Node node) {
- if (null == node) return false;
- for (int i = node.mData.size() - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mBigrams) return true;
- if (hasBigramsInternal(group.mChildren)) return true;
+ private static boolean hasBigramsInternal(final PtNodeArray nodeArray) {
+ if (null == nodeArray) return false;
+ for (int i = nodeArray.mData.size() - 1; i >= 0; --i) {
+ PtNode ptNode = nodeArray.mData.get(i);
+ if (null != ptNode.mBigrams) return true;
+ if (hasBigramsInternal(ptNode.mChildren)) return true;
}
return false;
}
@@ -729,7 +732,7 @@ public final class FusionDictionary implements Iterable<Word> {
// find a more efficient way of doing this, without compromising too much on memory
// and ease of use.
public boolean hasBigrams() {
- return hasBigramsInternal(mRoot);
+ return hasBigramsInternal(mRootNodeArray);
}
// Historically, the tails of the words were going to be merged to save space.
@@ -747,16 +750,16 @@ public final class FusionDictionary implements Iterable<Word> {
MakedictLog.i("Do not merge tails");
return;
-// MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
-// MakedictLog.i("Number of groups : " + countCharGroups(root));
+// MakedictLog.i("Merging PtNodes. Number of PtNodes : " + countPtNodes(root));
+// MakedictLog.i("Number of PtNodes : " + countPtNodes(root));
//
-// final HashMap<String, ArrayList<Node>> repository =
-// new HashMap<String, ArrayList<Node>>();
+// final HashMap<String, ArrayList<PtNodeArray>> repository =
+// new HashMap<String, ArrayList<PtNodeArray>>();
// mergeTailsInner(repository, root);
//
// MakedictLog.i("Number of different pseudohashes : " + repository.size());
// int size = 0;
-// for (ArrayList<Node> a : repository.values()) {
+// for (ArrayList<PtNodeArray> a : repository.values()) {
// size += a.size();
// }
// MakedictLog.i("Number of nodes after merge : " + (1 + size));
@@ -764,58 +767,58 @@ public final class FusionDictionary implements Iterable<Word> {
}
// The following methods are used by the deactivated mergeTails()
-// private static boolean isEqual(Node a, Node b) {
+// private static boolean isEqual(PtNodeArray a, PtNodeArray b) {
// if (null == a && null == b) return true;
// if (null == a || null == b) return false;
// if (a.data.size() != b.data.size()) return false;
// final int size = a.data.size();
// for (int i = size - 1; i >= 0; --i) {
-// CharGroup aGroup = a.data.get(i);
-// CharGroup bGroup = b.data.get(i);
-// if (aGroup.frequency != bGroup.frequency) return false;
-// if (aGroup.alternates == null && bGroup.alternates != null) return false;
-// if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
-// if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
-// if (!isEqual(aGroup.children, bGroup.children)) return false;
+// PtNode aPtNode = a.data.get(i);
+// PtNode bPtNode = b.data.get(i);
+// if (aPtNode.frequency != bPtNode.frequency) return false;
+// if (aPtNode.alternates == null && bPtNode.alternates != null) return false;
+// if (aPtNode.alternates != null && !aPtNode.equals(bPtNode.alternates)) return false;
+// if (!Arrays.equals(aPtNode.chars, bPtNode.chars)) return false;
+// if (!isEqual(aPtNode.children, bPtNode.children)) return false;
// }
// return true;
// }
-// static private HashMap<String, ArrayList<Node>> mergeTailsInner(
-// final HashMap<String, ArrayList<Node>> map, final Node node) {
-// final ArrayList<CharGroup> branches = node.data;
+// static private HashMap<String, ArrayList<PtNodeArray>> mergeTailsInner(
+// final HashMap<String, ArrayList<PtNodeArray>> map, final PtNodeArray nodeArray) {
+// final ArrayList<PtNode> branches = nodeArray.data;
// final int nodeSize = branches.size();
// for (int i = 0; i < nodeSize; ++i) {
-// CharGroup group = branches.get(i);
-// if (null != group.children) {
-// String pseudoHash = getPseudoHash(group.children);
-// ArrayList<Node> similarList = map.get(pseudoHash);
+// PtNode ptNode = branches.get(i);
+// if (null != ptNode.children) {
+// String pseudoHash = getPseudoHash(ptNode.children);
+// ArrayList<PtNodeArray> similarList = map.get(pseudoHash);
// if (null == similarList) {
-// similarList = new ArrayList<Node>();
+// similarList = new ArrayList<PtNodeArray>();
// map.put(pseudoHash, similarList);
// }
// boolean merged = false;
-// for (Node similar : similarList) {
-// if (isEqual(group.children, similar)) {
-// group.children = similar;
+// for (PtNodeArray similar : similarList) {
+// if (isEqual(ptNode.children, similar)) {
+// ptNode.children = similar;
// merged = true;
// break;
// }
// }
// if (!merged) {
-// similarList.add(group.children);
+// similarList.add(ptNode.children);
// }
-// mergeTailsInner(map, group.children);
+// mergeTailsInner(map, ptNode.children);
// }
// }
// return map;
// }
-// private static String getPseudoHash(final Node node) {
+// private static String getPseudoHash(final PtNodeArray nodeArray) {
// StringBuilder s = new StringBuilder();
-// for (CharGroup g : node.data) {
-// s.append(g.frequency);
-// for (int ch : g.chars) {
+// for (PtNode ptNode : nodeArray.data) {
+// s.append(ptNode.frequency);
+// for (int ch : ptNode.chars) {
// s.append(Character.toChars(ch));
// }
// }
@@ -829,20 +832,20 @@ public final class FusionDictionary implements Iterable<Word> {
*/
public static final class DictionaryIterator implements Iterator<Word> {
private static final class Position {
- public Iterator<CharGroup> pos;
+ public Iterator<PtNode> pos;
public int length;
- public Position(ArrayList<CharGroup> groups) {
- pos = groups.iterator();
+ public Position(ArrayList<PtNode> ptNodes) {
+ pos = ptNodes.iterator();
length = 0;
}
}
final StringBuilder mCurrentString;
final LinkedList<Position> mPositions;
- public DictionaryIterator(ArrayList<CharGroup> root) {
+ public DictionaryIterator(ArrayList<PtNode> ptRoot) {
mCurrentString = new StringBuilder();
mPositions = new LinkedList<Position>();
- final Position rootPos = new Position(root);
+ final Position rootPos = new Position(ptRoot);
mPositions.add(rootPos);
}
@@ -863,20 +866,20 @@ public final class FusionDictionary implements Iterable<Word> {
do {
if (currentPos.pos.hasNext()) {
- final CharGroup currentGroup = currentPos.pos.next();
+ final PtNode currentPtNode = currentPos.pos.next();
currentPos.length = mCurrentString.length();
- for (int i : currentGroup.mChars) {
+ for (int i : currentPtNode.mChars) {
mCurrentString.append(Character.toChars(i));
}
- if (null != currentGroup.mChildren) {
- currentPos = new Position(currentGroup.mChildren.mData);
+ if (null != currentPtNode.mChildren) {
+ currentPos = new Position(currentPtNode.mChildren.mData);
currentPos.length = mCurrentString.length();
mPositions.addLast(currentPos);
}
- if (currentGroup.mFrequency >= 0) {
- return new Word(mCurrentString.toString(), currentGroup.mFrequency,
- currentGroup.mShortcutTargets, currentGroup.mBigrams,
- currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
+ if (currentPtNode.mFrequency >= 0) {
+ return new Word(mCurrentString.toString(), currentPtNode.mFrequency,
+ currentPtNode.mShortcutTargets, currentPtNode.mBigrams,
+ currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry);
}
} else {
mPositions.removeLast();
@@ -901,6 +904,6 @@ public final class FusionDictionary implements Iterable<Word> {
*/
@Override
public Iterator<Word> iterator() {
- return new DictionaryIterator(mRoot.mData);
+ return new DictionaryIterator(mRootNodeArray.mData);
}
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java
index b3617443e..188de7a0f 100644
--- a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java
@@ -21,9 +21,9 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import java.util.ArrayList;
/**
- * Raw char group info straight out of a file. This will contain numbers for addresses.
+ * Raw PtNode info straight out of a file. This will contain numbers for addresses.
*/
-public final class CharGroupInfo {
+public final class PtNodeInfo {
public final int mOriginalAddress;
public final int mEndAddress;
@@ -35,7 +35,7 @@ public final class CharGroupInfo {
public final ArrayList<WeightedString> mShortcutTargets;
public final ArrayList<PendingAttribute> mBigrams;
- public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+ public PtNodeInfo(final int originalAddress, final int endAddress, final int flags,
final int[] characters, final int frequency, final int parentAddress,
final int childrenAddress, final ArrayList<WeightedString> shortcutTargets,
final ArrayList<PendingAttribute> bigrams) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
new file mode 100644
index 000000000..1a5023ef6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
@@ -0,0 +1,350 @@
+/*
+ * 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.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.JniUtils;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.TreeMap;
+
+/**
+ * An implementation of DictDecoder for version 3 binary dictionary.
+ */
+@UsedForTesting
+public class Ver3DictDecoder implements DictDecoder {
+ private static final String TAG = Ver3DictDecoder.class.getSimpleName();
+
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
+ // TODO: implement something sensical instead of just a phony method
+ private static native int doNothing();
+
+ private final static class HeaderReader {
+ protected static int readVersion(final DictBuffer dictBuffer)
+ throws IOException, UnsupportedFormatException {
+ return BinaryDictDecoderUtils.checkFormatVersion(dictBuffer);
+ }
+
+ protected static int readOptionFlags(final DictBuffer dictBuffer) {
+ return dictBuffer.readUnsignedShort();
+ }
+
+ protected static int readHeaderSize(final DictBuffer dictBuffer) {
+ return dictBuffer.readInt();
+ }
+
+ protected static HashMap<String, String> readAttributes(final DictBuffer dictBuffer,
+ final int headerSize) {
+ final HashMap<String, String> attributes = new HashMap<String, String>();
+ while (dictBuffer.position() < headerSize) {
+ // We can avoid an infinite loop here since dictBuffer.position() is always
+ // increased by calling CharEncoding.readString.
+ final String key = CharEncoding.readString(dictBuffer);
+ final String value = CharEncoding.readString(dictBuffer);
+ attributes.put(key, value);
+ }
+ dictBuffer.position(headerSize);
+ return attributes;
+ }
+ }
+
+ private final static class PtNodeReader {
+ protected static int readPtNodeOptionFlags(final DictBuffer dictBuffer) {
+ return dictBuffer.readUnsignedByte();
+ }
+
+ protected static int readParentAddress(final DictBuffer dictBuffer,
+ final FormatOptions formatOptions) {
+ if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+ return BinaryDictDecoderUtils.readSInt24(dictBuffer);
+ } else {
+ return FormatSpec.NO_PARENT_ADDRESS;
+ }
+ }
+
+ protected static int readFrequency(final DictBuffer dictBuffer) {
+ return dictBuffer.readUnsignedByte();
+ }
+
+ protected static int readChildrenAddress(final DictBuffer dictBuffer, final int optionFlags,
+ final FormatOptions formatOptions) {
+ if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+ final int address = BinaryDictDecoderUtils.readSInt24(dictBuffer);
+ if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
+ return address;
+ } else {
+ switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
+ return dictBuffer.readUnsignedByte();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES:
+ return dictBuffer.readUnsignedShort();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES:
+ return dictBuffer.readUnsignedInt24();
+ case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS:
+ default:
+ return FormatSpec.NO_CHILDREN_ADDRESS;
+ }
+ }
+ }
+
+ // Reads shortcuts and returns the read length.
+ protected static int readShortcut(final DictBuffer dictBuffer,
+ final ArrayList<WeightedString> shortcutTargets) {
+ final int pointerBefore = dictBuffer.position();
+ dictBuffer.readUnsignedShort(); // skip the size
+ while (true) {
+ final int targetFlags = dictBuffer.readUnsignedByte();
+ final String word = CharEncoding.readString(dictBuffer);
+ shortcutTargets.add(new WeightedString(word,
+ targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
+ if (0 == (targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
+ }
+ return dictBuffer.position() - pointerBefore;
+ }
+
+ protected static int readBigrams(final DictBuffer dictBuffer,
+ final ArrayList<PendingAttribute> bigrams, final int baseAddress) {
+ int readLength = 0;
+ int bigramCount = 0;
+ while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ final int bigramFlags = dictBuffer.readUnsignedByte();
+ ++readLength;
+ final int sign = 0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE)
+ ? 1 : -1;
+ int bigramAddress = baseAddress + readLength;
+ switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
+ bigramAddress += sign * dictBuffer.readUnsignedByte();
+ readLength += 1;
+ break;
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
+ bigramAddress += sign * dictBuffer.readUnsignedShort();
+ readLength += 2;
+ break;
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
+ final int offset = (dictBuffer.readUnsignedByte() << 16)
+ + dictBuffer.readUnsignedShort();
+ bigramAddress += sign * offset;
+ readLength += 3;
+ break;
+ default:
+ throw new RuntimeException("Has bigrams with no address");
+ }
+ bigrams.add(new PendingAttribute(
+ bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
+ bigramAddress));
+ if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
+ }
+ return readLength;
+ }
+ }
+
+ private final File mDictionaryBinaryFile;
+ private final DictionaryBufferFactory mBufferFactory;
+ private DictBuffer mDictBuffer;
+
+ public Ver3DictDecoder(final File file) {
+ this(file, USE_READONLY_BYTEBUFFER);
+ }
+
+ public Ver3DictDecoder(final File file, final int factoryFlag) {
+ mDictionaryBinaryFile = file;
+ mDictBuffer = null;
+
+ if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) {
+ mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory();
+ } else if ((factoryFlag & MASK_DICTBUFFER) == USE_BYTEARRAY) {
+ mBufferFactory = new DictionaryBufferFromByteArrayFactory();
+ } else if ((factoryFlag & MASK_DICTBUFFER) == USE_WRITABLE_BYTEBUFFER) {
+ mBufferFactory = new DictionaryBufferFromWritableByteBufferFactory();
+ } else {
+ mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory();
+ }
+ }
+
+ public Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) {
+ mDictionaryBinaryFile = file;
+ mBufferFactory = factory;
+ }
+
+ public void openDictBuffer() throws FileNotFoundException, IOException {
+ mDictBuffer = mBufferFactory.getDictionaryBuffer(mDictionaryBinaryFile);
+ }
+
+ /* package */ DictBuffer getDictBuffer() {
+ return mDictBuffer;
+ }
+
+ @UsedForTesting
+ /* package */ DictBuffer openAndGetDictBuffer() throws FileNotFoundException, IOException {
+ openDictBuffer();
+ return getDictBuffer();
+ }
+
+ @Override
+ public FileHeader readHeader() throws IOException, UnsupportedFormatException {
+ if (mDictBuffer == null) {
+ openDictBuffer();
+ }
+
+ final int version = HeaderReader.readVersion(mDictBuffer);
+ final int optionsFlags = HeaderReader.readOptionFlags(mDictBuffer);
+
+ final int headerSize = HeaderReader.readHeaderSize(mDictBuffer);
+
+ if (headerSize < 0) {
+ throw new UnsupportedFormatException("header size can't be negative.");
+ }
+
+ final HashMap<String, String> attributes = HeaderReader.readAttributes(mDictBuffer,
+ headerSize);
+
+ final FileHeader header = new FileHeader(headerSize,
+ new FusionDictionary.DictionaryOptions(attributes,
+ 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
+ 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
+ new FormatOptions(version,
+ 0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE)));
+ return header;
+ }
+
+ // TODO: Make this buffer multi thread safe.
+ private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
+ @Override
+ public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions options) {
+ int addressPointer = ptNodePos;
+ final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
+ ++addressPointer;
+
+ final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options);
+ if (BinaryDictIOUtils.supportsDynamicUpdate(options)) {
+ addressPointer += 3;
+ }
+
+ final int characters[];
+ if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) {
+ int index = 0;
+ int character = CharEncoding.readChar(mDictBuffer);
+ addressPointer += CharEncoding.getCharSize(character);
+ while (-1 != character) {
+ // FusionDictionary is making sure that the length of the word is smaller than
+ // MAX_WORD_LENGTH.
+ // So we'll never write past the end of mCharacterBuffer.
+ mCharacterBuffer[index++] = character;
+ character = CharEncoding.readChar(mDictBuffer);
+ addressPointer += CharEncoding.getCharSize(character);
+ }
+ characters = Arrays.copyOfRange(mCharacterBuffer, 0, index);
+ } else {
+ final int character = CharEncoding.readChar(mDictBuffer);
+ addressPointer += CharEncoding.getCharSize(character);
+ characters = new int[] { character };
+ }
+ final int frequency;
+ if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
+ ++addressPointer;
+ frequency = PtNodeReader.readFrequency(mDictBuffer);
+ } else {
+ frequency = PtNode.NOT_A_TERMINAL;
+ }
+ int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options);
+ if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
+ childrenAddress += addressPointer;
+ }
+ addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options);
+ final ArrayList<WeightedString> shortcutTargets;
+ if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
+ // readShortcut will add shortcuts to shortcutTargets.
+ shortcutTargets = new ArrayList<WeightedString>();
+ addressPointer += PtNodeReader.readShortcut(mDictBuffer, shortcutTargets);
+ } else {
+ shortcutTargets = null;
+ }
+
+ final ArrayList<PendingAttribute> bigrams;
+ if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
+ bigrams = new ArrayList<PendingAttribute>();
+ addressPointer += PtNodeReader.readBigrams(mDictBuffer, bigrams, addressPointer);
+ if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ MakedictLog.d("too many bigrams in a PtNode.");
+ }
+ } else {
+ bigrams = null;
+ }
+ return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency,
+ parentAddress, childrenAddress, shortcutTargets, bigrams);
+ }
+
+ @Override
+ public FusionDictionary readDictionaryBinary(final FusionDictionary dict)
+ throws FileNotFoundException, IOException, UnsupportedFormatException {
+ if (mDictBuffer == null) {
+ openDictBuffer();
+ }
+ try {
+ return BinaryDictDecoderUtils.readDictionaryBinary(this, dict);
+ } catch (IOException e) {
+ Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e);
+ if (!mDictionaryBinaryFile.delete()) {
+ Log.e(TAG, "Failed to delete the broken dictionary.");
+ }
+ throw e;
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e);
+ if (!mDictionaryBinaryFile.delete()) {
+ Log.e(TAG, "Failed to delete the broken dictionary.");
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public int getTerminalPosition(String word) throws IOException, UnsupportedFormatException {
+ if (mDictBuffer == null) {
+ openDictBuffer();
+ }
+ return BinaryDictIOUtils.getTerminalPosition(this, word);
+ }
+
+ @Override
+ public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words,
+ final TreeMap<Integer, Integer> frequencies,
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams)
+ throws IOException, UnsupportedFormatException {
+ if (mDictBuffer == null) {
+ openDictBuffer();
+ }
+ BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams);
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
new file mode 100644
index 000000000..e81fd4514
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.FormatSpec.FormatOptions;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An implementation of DictEncoder for version 3 binary dictionary.
+ */
+public class Ver3DictEncoder implements DictEncoder {
+
+ private final File mDictFile;
+ private OutputStream mOutStream;
+
+ public Ver3DictEncoder(final File dictFile) {
+ mDictFile = dictFile;
+ mOutStream = null;
+ }
+
+ // This constructor is used only by BinaryDictOffdeviceUtilsTests.
+ // If you want to use this in the production code, you should consider keeping consistency of
+ // the interface of Ver3DictDecoder by using factory.
+ public Ver3DictEncoder(final OutputStream outStream) {
+ mDictFile = null;
+ mOutStream = outStream;
+ }
+
+ private void openStream() throws FileNotFoundException {
+ mOutStream = new FileOutputStream(mDictFile);
+ }
+
+ private void close() throws IOException {
+ if (mOutStream != null) {
+ mOutStream.close();
+ mOutStream = null;
+ }
+ }
+
+ @Override
+ public void writeDictionary(FusionDictionary dict, FormatOptions formatOptions)
+ throws IOException, UnsupportedFormatException {
+ if (mOutStream == null) {
+ openStream();
+ }
+ BinaryDictEncoderUtils.writeDictionaryBinary(mOutStream, dict, formatOptions);
+ close();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
new file mode 100644
index 000000000..d44660623
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
@@ -0,0 +1,159 @@
+/*
+ * 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.content.Context;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.AbstractDictionaryWriter;
+import com.android.inputmethod.latin.ExpandableDictionary;
+import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.ExpandableDictionary.NextWord;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.makedict.DictEncoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils;
+import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.BigramDictionaryInterface;
+import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
+import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+// Currently this class is used to implement dynamic prodiction dictionary.
+// TODO: Move to native code.
+public class DynamicPersonalizationDictionaryWriter extends AbstractDictionaryWriter {
+ private static final String TAG = DynamicPersonalizationDictionaryWriter.class.getSimpleName();
+ /** Maximum number of pairs. Pruning will start when databases goes above this number. */
+ public static final int MAX_HISTORY_BIGRAMS = 10000;
+
+ /** Any pair being typed or picked */
+ private static final int FREQUENCY_FOR_TYPED = 2;
+
+ private static final int BINARY_DICT_VERSION = 3;
+ private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
+ new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */);
+
+ private final UserHistoryDictionaryBigramList mBigramList =
+ new UserHistoryDictionaryBigramList();
+ private final ExpandableDictionary mExpandableDictionary;
+
+ public DynamicPersonalizationDictionaryWriter(final Context context, final String dictType) {
+ super(context, dictType);
+ mExpandableDictionary = new ExpandableDictionary(dictType);
+ }
+
+ @Override
+ public void clear() {
+ mBigramList.evictAll();
+ mExpandableDictionary.clearDictionary();
+ }
+
+ /**
+ * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
+ * are done to update the binary dictionary.
+ */
+ @Override
+ public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
+ final boolean isNotAWord) {
+ mExpandableDictionary.addWord(word, shortcutTarget, frequency);
+ mBigramList.addBigram(null, word, (byte)frequency);
+ }
+
+ @Override
+ public void addBigramWords(final String word0, final String word1, final int frequency,
+ final boolean isValid, final long lastModifiedTime) {
+ if (lastModifiedTime > 0) {
+ mExpandableDictionary.setBigramAndGetFrequency(word0, word1,
+ new ForgettingCurveParams(frequency, System.currentTimeMillis(),
+ lastModifiedTime));
+ mBigramList.addBigram(word0, word1, (byte)frequency);
+ } else {
+ mExpandableDictionary.setBigramAndGetFrequency(word0, word1,
+ new ForgettingCurveParams(isValid));
+ mBigramList.addBigram(word0, word1, (byte)frequency);
+ }
+ }
+
+ @Override
+ public void removeBigramWords(final String word0, final String word1) {
+ if (mBigramList.removeBigram(word0, word1)) {
+ mExpandableDictionary.removeBigram(word0, word1);
+ }
+ }
+
+ @Override
+ protected void writeDictionary(final DictEncoder dictEncoder)
+ throws IOException, UnsupportedFormatException {
+ UserHistoryDictIOUtils.writeDictionary(dictEncoder,
+ new FrequencyProvider(mBigramList, mExpandableDictionary), mBigramList,
+ FORMAT_OPTIONS);
+ }
+
+ private static class FrequencyProvider implements BigramDictionaryInterface {
+ final private UserHistoryDictionaryBigramList mBigramList;
+ final private ExpandableDictionary mExpandableDictionary;
+
+ public FrequencyProvider(final UserHistoryDictionaryBigramList bigramList,
+ final ExpandableDictionary expandableDictionary) {
+ mBigramList = bigramList;
+ mExpandableDictionary = expandableDictionary;
+ }
+ @Override
+ public int getFrequency(final String word0, final String word1) {
+ final int freq;
+ if (word0 == null) { // unigram
+ freq = FREQUENCY_FOR_TYPED;
+ } else { // bigram
+ final NextWord nw = mExpandableDictionary.getBigramWord(word0, word1);
+ if (nw != null) {
+ final ForgettingCurveParams forgettingCurveParams = nw.getFcParams();
+ final byte prevFc = mBigramList.getBigrams(word0).get(word1);
+ final byte fc = forgettingCurveParams.getFc();
+ final boolean isValid = forgettingCurveParams.isValid();
+ if (prevFc > 0 && prevFc == fc) {
+ freq = fc & 0xFF;
+ } else if (UserHistoryForgettingCurveUtils.
+ needsToSave(fc, isValid, mBigramList.size() <= MAX_HISTORY_BIGRAMS)) {
+ freq = fc & 0xFF;
+ } else {
+ // Delete this entry
+ freq = -1;
+ }
+ } else {
+ // Delete this entry
+ freq = -1;
+ }
+ }
+ return freq;
+ }
+ }
+
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final String prevWord, final ProximityInfo proximityInfo,
+ boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ return mExpandableDictionary.getSuggestions(composer, prevWord, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions);
+ }
+
+ @Override
+ public boolean isValidWord(final String word) {
+ return mExpandableDictionary.isValidWord(word);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
index bb6ec6b1c..a08145b33 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
@@ -18,59 +18,41 @@ package com.android.inputmethod.latin.personalization;
import android.content.Context;
import android.content.SharedPreferences;
-import android.os.AsyncTask;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.ExpandableDictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.WordComposer;
-import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.DictDecoder;
+import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
import com.android.inputmethod.latin.settings.Settings;
-import com.android.inputmethod.latin.utils.ByteArrayWrapper;
import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils;
-import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.BigramDictionaryInterface;
import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.concurrent.locks.ReentrantLock;
/**
* This class is a base class of a dictionary for the personalized prediction language model.
*/
-public abstract class DynamicPredictionDictionaryBase extends ExpandableDictionary {
-
+public abstract class DynamicPredictionDictionaryBase extends ExpandableBinaryDictionary {
private static final String TAG = DynamicPredictionDictionaryBase.class.getSimpleName();
public static final boolean DBG_SAVE_RESTORE = false;
private static final boolean DBG_STRESS_TEST = false;
private static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG;
- private static final FormatOptions VERSION3 = new FormatOptions(3,
- true /* supportsDynamicUpdate */);
-
/** Any pair being typed or picked */
- private static final int FREQUENCY_FOR_TYPED = 2;
-
- /** Maximum number of pairs. Pruning will start when databases goes above this number. */
- private static final int MAX_HISTORY_BIGRAMS = 10000;
+ public static final int FREQUENCY_FOR_TYPED = 2;
/** Locale for which this user history dictionary is storing words */
private final String mLocale;
- private final UserHistoryDictionaryBigramList mBigramList =
- new UserHistoryDictionaryBigramList();
- private final ReentrantLock mBigramListLock = new ReentrantLock();
+ private final String mFileName;
+
private final SharedPreferences mPrefs;
private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions =
@@ -80,41 +62,44 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona
@UsedForTesting boolean mIsTest = false;
/* package */ DynamicPredictionDictionaryBase(final Context context, final String locale,
- final SharedPreferences sp, final String dictionaryType) {
- super(context, dictionaryType);
+ final SharedPreferences sp, final String dictionaryType, final String fileName) {
+ super(context, fileName, dictionaryType, true);
mLocale = locale;
+ mFileName = fileName;
mPrefs = sp;
if (mLocale != null && mLocale.length() > 1) {
- loadDictionary();
+ asyncLoadDictionaryToMemory();
+ asyncReloadDictionaryIfRequired();
}
}
@Override
public void close() {
- flushPendingWrites();
- // Don't close the database as locale changes will require it to be reopened anyway
- // Also, the database is written to somewhat frequently, so it needs to be kept alive
- // throughout the life of the process.
- // mOpenHelper.close();
- // Ignore close because we cache PersonalizationPredictionDictionary for each language.
- // See getInstance() above.
+ // Close only binary dictionary to reuse this dictionary.
// super.close();
+ closeBinaryDictionary();
+ // Flush pending writes.
+ // TODO: Remove after this class become to use a dynamic binary dictionary.
+ asyncWriteBinaryDictionary();
+ Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale);
}
@Override
- protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
- // Inhibit suggestions (not predictions) for user history for now. Removing this method
- // is enough to use it through the standard ExpandableDictionary way.
- return null;
+ protected boolean hasContentChanged() {
+ return false;
+ }
+
+ @Override
+ protected boolean needsToReloadBeforeWriting() {
+ return false;
}
/**
* Return whether the passed charsequence is in the dictionary.
*/
@Override
- public synchronized boolean isValidWord(final String word) {
- // TODO: figure out what is the correct thing to do here.
+ public boolean isValidWord(final String word) {
+ // Words included only in the user history should be treated as not in dictionary words.
return false;
}
@@ -126,70 +111,29 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona
* context, as in beginning of a sentence for example.
* The second word may not be null (a NullPointerException would be thrown).
*/
- public int addToPersonalizationPredictionDictionary(
- final String word1, final String word2, final boolean isValid) {
- if (word2.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH ||
- (word1 != null && word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) {
- return -1;
+ public void addToPersonalizationPredictionDictionary(
+ 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;
}
- if (mBigramListLock.tryLock()) {
- try {
- super.addWord(
- word2, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED);
- mBigramList.addBigram(null, word2, (byte)FREQUENCY_FOR_TYPED);
- // Do not insert a word as a bigram of itself
- if (word2.equals(word1)) {
- return 0;
- }
- final int freq;
- if (null == word1) {
- freq = FREQUENCY_FOR_TYPED;
- } else {
- freq = super.setBigramAndGetFrequency(
- word1, word2, new ForgettingCurveParams(isValid));
- }
- mBigramList.addBigram(word1, word2);
- return freq;
- } finally {
- mBigramListLock.unlock();
- }
+ addWordDynamically(word1, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED,
+ false /* isNotAWord */);
+ // Do not insert a word as a bigram of itself
+ if (word1.equals(word0)) {
+ return;
}
- return -1;
- }
-
- public boolean cancelAddingUserHistory(final String word1, final String word2) {
- if (mBigramListLock.tryLock()) {
- try {
- if (mBigramList.removeBigram(word1, word2)) {
- return super.removeBigram(word1, word2);
- }
- } finally {
- mBigramListLock.unlock();
- }
+ if (null != word0) {
+ addBigramDynamically(word0, word1, FREQUENCY_FOR_TYPED, isValid);
}
- return false;
}
- /**
- * Schedules a background thread to write any pending words to the database.
- */
- private void flushPendingWrites() {
- // Create a background thread to write the pending entries
- new UpdateBinaryTask(mBigramList, mLocale, this, mPrefs, getContext()).execute();
+ public void cancelAddingUserHistory(final String word0, final String word1) {
+ removeBigramDynamically(word0, word1);
}
@Override
- public final void loadDictionaryAsync() {
- // This must be run on non-main thread
- mBigramListLock.lock();
- try {
- loadDictionaryAsyncLocked();
- } finally {
- mBigramListLock.unlock();
- }
- }
-
- private void loadDictionaryAsyncLocked() {
+ protected void loadDictionaryAsync() {
final int[] profTotalCount = { 0 };
final String locale = getLocale();
if (DBG_STRESS_TEST) {
@@ -201,10 +145,8 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona
}
}
final long last = Settings.readLastUserHistoryWriteTime(mPrefs, locale);
- final boolean initializing = last == 0;
final long now = System.currentTimeMillis();
- final String fileName = getDictionaryFileName();
- final ExpandableDictionary dictionary = this;
+ final ExpandableBinaryDictionary dictionary = this;
final OnAddWordListener listener = new OnAddWordListener() {
@Override
public void setUnigram(final String word, final String shortcutTarget,
@@ -212,49 +154,36 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona
if (DBG_SAVE_RESTORE) {
Log.d(TAG, "load unigram: " + word + "," + frequency);
}
- dictionary.addWord(word, shortcutTarget, frequency);
+ addWord(word, shortcutTarget, frequency, false /* isNotAWord */);
++profTotalCount[0];
- addToBigramListLocked(null, word, (byte)frequency);
}
@Override
- public void setBigram(final String word1, final String word2, final int frequency) {
- if (word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH
- && word2.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) {
+ public void setBigram(final String word0, final String word1, final int frequency) {
+ if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH
+ && word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) {
if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "load bigram: " + word1 + "," + word2 + "," + frequency);
+ Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency);
}
++profTotalCount[0];
- dictionary.setBigramAndGetFrequency(
- word1, word2, initializing ? new ForgettingCurveParams(true)
- : new ForgettingCurveParams(frequency, now, last));
+ addBigram(word0, word1, frequency, last);
}
- addToBigramListLocked(word1, word2, (byte)frequency);
}
};
// Load the dictionary from binary file
- FileInputStream inStream = null;
+ final File dictFile = new File(mContext.getFilesDir(), mFileName);
+ final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(dictFile,
+ DictDecoder.USE_BYTEARRAY);
try {
- final File file = new File(getContext().getFilesDir(), fileName);
- final byte[] buffer = new byte[(int)file.length()];
- inStream = new FileInputStream(file);
- inStream.read(buffer);
- UserHistoryDictIOUtils.readDictionaryBinary(
- new ByteArrayWrapper(buffer), listener);
+ dictDecoder.openDictBuffer();
+ UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener);
} catch (FileNotFoundException e) {
// This is an expected condition: we don't have a user history dictionary for this
// language yet. It will be created sometime later.
} catch (IOException e) {
Log.e(TAG, "IOException on opening a bytebuffer", e);
} finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
if (PROFILE_SAVE_RESTORE) {
final long diff = System.currentTimeMillis() - now;
Log.d(TAG, "PROF: Load UserHistoryDictionary: "
@@ -263,146 +192,30 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableDictiona
}
}
- protected abstract String getDictionaryFileName();
-
protected String getLocale() {
return mLocale;
}
- private void addToBigramListLocked(String word0, String word1, byte fcValue) {
- mBigramList.addBigram(word0, word1, fcValue);
- }
-
- /**
- * Async task to write pending words to the binarydicts.
- */
- private static final class UpdateBinaryTask extends AsyncTask<Void, Void, Void>
- implements BigramDictionaryInterface {
- private final UserHistoryDictionaryBigramList mBigramList;
- private final boolean mAddLevel0Bigrams;
- private final String mLocale;
- private final DynamicPredictionDictionaryBase mDynamicPredictionDictionary;
- private final SharedPreferences mPrefs;
- private final Context mContext;
-
- public UpdateBinaryTask(final UserHistoryDictionaryBigramList pendingWrites,
- final String locale, final DynamicPredictionDictionaryBase dict,
- final SharedPreferences prefs, final Context context) {
- mBigramList = pendingWrites;
- mLocale = locale;
- mDynamicPredictionDictionary = dict;
- mPrefs = prefs;
- mContext = context;
- mAddLevel0Bigrams = mBigramList.size() <= MAX_HISTORY_BIGRAMS;
- }
-
- @Override
- protected Void doInBackground(final Void... v) {
- if (mDynamicPredictionDictionary.mIsTest) {
- // If mIsTest == true, wait until the lock is released.
- mDynamicPredictionDictionary.mBigramListLock.lock();
- try {
- doWriteTaskLocked();
- } finally {
- mDynamicPredictionDictionary.mBigramListLock.unlock();
- }
- } else if (mDynamicPredictionDictionary.mBigramListLock.tryLock()) {
- try {
- doWriteTaskLocked();
- } finally {
- mDynamicPredictionDictionary.mBigramListLock.unlock();
- }
- }
- return null;
- }
-
- private void doWriteTaskLocked() {
- if (DBG_STRESS_TEST) {
- try {
- Log.w(TAG, "Start stress in closing: " + mLocale);
- Thread.sleep(15000);
- Log.w(TAG, "End stress in closing");
- } catch (InterruptedException e) {
- Log.e(TAG, "In stress test", e);
- }
- }
-
- final long now = PROFILE_SAVE_RESTORE ? System.currentTimeMillis() : 0;
- final String fileName =
- mDynamicPredictionDictionary.getDictionaryFileName();
- final File file = new File(mContext.getFilesDir(), fileName);
- FileOutputStream out = null;
-
- try {
- out = new FileOutputStream(file);
- UserHistoryDictIOUtils.writeDictionaryBinary(out, this, mBigramList, VERSION3);
- out.flush();
- out.close();
- } catch (IOException e) {
- Log.e(TAG, "IO Exception while writing file", e);
- } finally {
- if (out != null) {
- try {
- out.close();
- } catch (IOException e) {
- // ignore
- }
- }
- }
-
- // Save the timestamp after we finish writing the binary dictionary.
- Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale);
- if (PROFILE_SAVE_RESTORE) {
- final long diff = System.currentTimeMillis() - now;
- Log.w(TAG, "PROF: Write User HistoryDictionary: " + mLocale + ", " + diff + "ms.");
- }
- }
-
- @Override
- public int getFrequency(final String word1, final String word2) {
- final int freq;
- if (word1 == null) { // unigram
- freq = FREQUENCY_FOR_TYPED;
- final byte prevFc = mBigramList.getBigrams(word1).get(word2);
- } else { // bigram
- final NextWord nw =
- mDynamicPredictionDictionary.getBigramWord(word1, word2);
- if (nw != null) {
- final ForgettingCurveParams fcp = nw.getFcParams();
- final byte prevFc = mBigramList.getBigrams(word1).get(word2);
- final byte fc = fcp.getFc();
- final boolean isValid = fcp.isValid();
- if (prevFc > 0 && prevFc == fc) {
- freq = fc & 0xFF;
- } else if (UserHistoryForgettingCurveUtils.
- needsToSave(fc, isValid, mAddLevel0Bigrams)) {
- freq = fc & 0xFF;
- } else {
- // Delete this entry
- freq = -1;
- }
- } else {
- // Delete this entry
- freq = -1;
- }
- }
- return freq;
- }
- }
-
@UsedForTesting
/* package for test */ void forceAddWordForTest(
- final String word1, final String word2, final boolean isValid) {
- mBigramListLock.lock();
- try {
- addToPersonalizationPredictionDictionary(word1, word2, isValid);
- } finally {
- mBigramListLock.unlock();
- }
+ final String word0, final String word1, final boolean isValid) {
+ addToPersonalizationPredictionDictionary(word0, word1, isValid);
}
public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) {
- session.setDictionary(this);
+ session.setPredictionDictionary(this);
mSessions.add(session);
+ session.onDictionaryReady();
+ }
+
+ public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) {
+ mSessions.remove(session);
+ }
+
+ public void clearAndFlushDictionary() {
+ // Clear the node structure on memory
+ clear();
+ // Then flush the cleared state of the dictionary on disk.
+ asyncWriteBinaryDictionary();
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
index e38a235e9..f257165cb 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -18,26 +18,32 @@ package com.android.inputmethod.latin.personalization;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.ArrayList;
/**
* This class is a dictionary for the personalized language model that uses binary dictionary.
*/
public class PersonalizationDictionary extends ExpandableBinaryDictionary {
private static final String NAME = "personalization";
-
- public static void registerUpdateListener(PersonalizationDictionaryUpdateSession listener) {
- // TODO: Implement
- }
+ private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions =
+ CollectionUtils.newArrayList();
/** Locale for which this user history dictionary is storing words */
private final String mLocale;
- // Singleton
- private PersonalizationDictionary(final Context context, final String locale) {
- super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION);
+ public PersonalizationDictionary(final Context context, final String locale,
+ final SharedPreferences prefs) {
+ // TODO: Make isUpdatable true.
+ super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION,
+ false /* isUpdatable */);
mLocale = locale;
+ // TODO: Restore last updated time
+ loadDictionary();
}
@Override
@@ -47,15 +53,21 @@ public class PersonalizationDictionary extends ExpandableBinaryDictionary {
@Override
protected boolean hasContentChanged() {
- // TODO: Implement
return false;
}
@Override
protected boolean needsToReloadBeforeWriting() {
- // TODO: Implement
return false;
}
- // TODO: Implement
+ public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) {
+ session.setDictionary(this);
+ mSessions.add(session);
+ session.onDictionaryReady();
+ }
+
+ public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) {
+ mSessions.remove(session);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
new file mode 100644
index 000000000..c1833ff14
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
@@ -0,0 +1,37 @@
+/*
+ * 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.content.Context;
+import android.content.res.Configuration;
+
+public class PersonalizationDictionarySessionRegister {
+ public static void init(Context context) {
+ }
+
+ public static void onConfigurationChanged(final Context context, final Configuration conf) {
+ }
+
+ public static void onUpdateData(Context context, String type) {
+ }
+
+ public static void onRemoveData(Context context, String type) {
+ }
+
+ public static void onDestroy(Context context) {
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
index d62aec19d..ab3de801c 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin.personalization;
+import android.content.Context;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -43,18 +45,78 @@ public abstract class PersonalizationDictionaryUpdateSession {
}
// TODO: Use a dynamic binary dictionary instead
- public WeakReference<DynamicPredictionDictionaryBase> mDictionary;
+ public WeakReference<PersonalizationDictionary> mDictionary;
+ public WeakReference<DynamicPredictionDictionaryBase> mPredictionDictionary;
+ public final String mSystemLocale;
+ public PersonalizationDictionaryUpdateSession(String locale) {
+ mSystemLocale = locale;
+ }
public abstract void onDictionaryReady();
- public void setDictionary(DynamicPredictionDictionaryBase dictionary) {
- mDictionary = new WeakReference<DynamicPredictionDictionaryBase>(dictionary);
+ public abstract void onDictionaryClosed(Context context);
+
+ public void setDictionary(PersonalizationDictionary dictionary) {
+ mDictionary = new WeakReference<PersonalizationDictionary>(dictionary);
+ }
+
+ public void setPredictionDictionary(DynamicPredictionDictionaryBase dictionary) {
+ mPredictionDictionary = new WeakReference<DynamicPredictionDictionaryBase>(dictionary);
+ }
+
+ protected PersonalizationDictionary getDictionary() {
+ return mDictionary == null ? null : mDictionary.get();
+ }
+
+ protected DynamicPredictionDictionaryBase getPredictionDictionary() {
+ return mPredictionDictionary == null ? null : mPredictionDictionary.get();
+ }
+
+ private void unsetDictionary() {
+ final PersonalizationDictionary dictionary = getDictionary();
+ if (dictionary == null) {
+ return;
+ }
+ dictionary.unRegisterUpdateSession(this);
+ }
+
+ private void unsetPredictionDictionary() {
+ final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
+ if (dictionary == null) {
+ return;
+ }
+ dictionary.unRegisterUpdateSession(this);
+ }
+
+ public void clearAndFlushPredictionDictionary(Context context) {
+ final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
+ if (dictionary == null) {
+ return;
+ }
+ dictionary.clearAndFlushDictionary();
+ }
+
+ public void closeSession(Context context) {
+ unsetDictionary();
+ unsetPredictionDictionary();
+ onDictionaryClosed(context);
+ }
+
+ // TODO: Support multi locale to add bigram
+ public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid,
+ int frequency) {
+ final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
+ if (dictionary == null) {
+ return;
+ }
+ dictionary.addToPersonalizationPredictionDictionary(word0, word1, isValid);
}
- public void addToPersonalizationDictionary(
+ // Bulk import
+ // TODO: Support multi locale to add bigram
+ public void addBigramsToPersonalizationDictionary(
final ArrayList<PersonalizationLanguageModelParam> lmParams) {
- final DynamicPredictionDictionaryBase dictionary = mDictionary == null
- ? null : mDictionary.get();
+ final DynamicPredictionDictionaryBase dictionary = getPredictionDictionary();
if (dictionary == null) {
return;
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
index da256f827..c8deaf90d 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryHelper.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -26,16 +26,20 @@ import android.util.Log;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
-public class PersonalizationDictionaryHelper {
- private static final String TAG = PersonalizationDictionaryHelper.class.getSimpleName();
+public class PersonalizationHelper {
+ private static final String TAG = PersonalizationHelper.class.getSimpleName();
private static final boolean DEBUG = false;
private static final ConcurrentHashMap<String, SoftReference<UserHistoryPredictionDictionary>>
sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap();
+ private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>>
+ sLangPersonalizationDictCache = CollectionUtils.newConcurrentHashMap();
+
private static final ConcurrentHashMap<String,
SoftReference<PersonalizationPredictionDictionary>>
- sLangPersonalizationDictCache = CollectionUtils.newConcurrentHashMap();
+ sLangPersonalizationPredictionDictCache =
+ CollectionUtils.newConcurrentHashMap();
public static UserHistoryPredictionDictionary getUserHistoryPredictionDictionary(
final Context context, final String locale, final SharedPreferences sp) {
@@ -48,6 +52,7 @@ public class PersonalizationDictionaryHelper {
if (DEBUG) {
Log.w(TAG, "Use cached UserHistoryPredictionDictionary for " + locale);
}
+ dict.asyncReloadDictionaryIfRequired();
return dict;
}
}
@@ -59,22 +64,46 @@ public class PersonalizationDictionaryHelper {
}
}
- public static void
- registerPersonalizationDictionaryUpdateSession(final Context context,
- final PersonalizationDictionaryUpdateSession session) {
- final PersonalizationPredictionDictionary dictionary =
- getPersonalizationPredictionDictionary(context,
- context.getResources().getConfiguration().locale.toString(),
+ public static void registerPersonalizationDictionaryUpdateSession(final Context context,
+ final PersonalizationDictionaryUpdateSession session, String locale) {
+ final PersonalizationPredictionDictionary predictionDictionary =
+ getPersonalizationPredictionDictionary(context, locale,
+ PreferenceManager.getDefaultSharedPreferences(context));
+ predictionDictionary.registerUpdateSession(session);
+ final PersonalizationDictionary dictionary =
+ getPersonalizationDictionary(context, locale,
PreferenceManager.getDefaultSharedPreferences(context));
dictionary.registerUpdateSession(session);
}
- public static PersonalizationPredictionDictionary getPersonalizationPredictionDictionary(
+ public static PersonalizationDictionary getPersonalizationDictionary(
final Context context, final String locale, final SharedPreferences sp) {
synchronized (sLangPersonalizationDictCache) {
if (sLangPersonalizationDictCache.containsKey(locale)) {
- final SoftReference<PersonalizationPredictionDictionary> ref =
+ final SoftReference<PersonalizationDictionary> ref =
sLangPersonalizationDictCache.get(locale);
+ final PersonalizationDictionary dict = ref == null ? null : ref.get();
+ if (dict != null) {
+ if (DEBUG) {
+ Log.w(TAG, "Use cached PersonalizationDictCache for " + locale);
+ }
+ return dict;
+ }
+ }
+ final PersonalizationDictionary dict =
+ new PersonalizationDictionary(context, locale, sp);
+ sLangPersonalizationDictCache.put(
+ locale, new SoftReference<PersonalizationDictionary>(dict));
+ return dict;
+ }
+ }
+
+ public static PersonalizationPredictionDictionary getPersonalizationPredictionDictionary(
+ final Context context, final String locale, final SharedPreferences sp) {
+ synchronized (sLangPersonalizationPredictionDictCache) {
+ if (sLangPersonalizationPredictionDictCache.containsKey(locale)) {
+ final SoftReference<PersonalizationPredictionDictionary> ref =
+ sLangPersonalizationPredictionDictCache.get(locale);
final PersonalizationPredictionDictionary dict = ref == null ? null : ref.get();
if (dict != null) {
if (DEBUG) {
@@ -85,7 +114,7 @@ public class PersonalizationDictionaryHelper {
}
final PersonalizationPredictionDictionary dict =
new PersonalizationPredictionDictionary(context, locale, sp);
- sLangPersonalizationDictCache.put(
+ sLangPersonalizationPredictionDictCache.put(
locale, new SoftReference<PersonalizationPredictionDictionary>(dict));
return dict;
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
index 955bd2762..e80953c05 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin.personalization;
import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import android.content.Context;
import android.content.SharedPreferences;
@@ -26,11 +27,11 @@ public class PersonalizationPredictionDictionary extends DynamicPredictionDictio
/* package */ PersonalizationPredictionDictionary(final Context context, final String locale,
final SharedPreferences sp) {
- super(context, locale, sp, Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA);
+ super(context, locale, sp, Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA,
+ getDictionaryFileName(locale));
}
- @Override
- protected String getDictionaryFileName() {
- return NAME + "." + getLocale() + ".dict";
+ private static String getDictionaryFileName(final String locale) {
+ return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java
index f21db25a6..6c2c9e26e 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java
@@ -45,6 +45,7 @@ public final class UserHistoryDictionaryBigramList {
/**
* Called when the user typed a word.
*/
+ @UsedForTesting
public void addBigram(String word1, String word2) {
addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE);
}
@@ -53,7 +54,7 @@ public final class UserHistoryDictionaryBigramList {
* Called when loaded from the SQL DB.
*/
public void addBigram(String word1, String word2, byte fcValue) {
- if (UserHistoryPredictionDictionary.DBG_SAVE_RESTORE) {
+ if (DynamicPredictionDictionaryBase.DBG_SAVE_RESTORE) {
Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue);
}
final HashMap<String, Byte> map;
@@ -73,7 +74,7 @@ public final class UserHistoryDictionaryBigramList {
* Called when inserted to the SQL DB.
*/
public void updateBigram(String word1, String word2, byte fcValue) {
- if (UserHistoryPredictionDictionary.DBG_SAVE_RESTORE) {
+ if (DynamicPredictionDictionaryBase.DBG_SAVE_RESTORE) {
Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue);
}
final HashMap<String, Byte> map;
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
index d11784454..b140c919b 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin.personalization;
import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import android.content.Context;
import android.content.SharedPreferences;
@@ -26,14 +27,14 @@ import android.content.SharedPreferences;
* cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
*/
public class UserHistoryPredictionDictionary extends DynamicPredictionDictionaryBase {
- private static final String NAME = UserHistoryPredictionDictionary.class.getSimpleName();
+ /* package for tests */ static final String NAME =
+ UserHistoryPredictionDictionary.class.getSimpleName();
/* package */ UserHistoryPredictionDictionary(final Context context, final String locale,
final SharedPreferences sp) {
- super(context, locale, sp, Dictionary.TYPE_USER_HISTORY);
+ super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, getDictionaryFileName(locale));
}
- @Override
- protected String getDictionaryFileName() {
- return NAME + "." + getLocale() + ".dict";
+ private static String getDictionaryFileName(final String locale) {
+ return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
index 139f5e290..6543003e8 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -40,8 +40,4 @@ public class AdditionalFeaturesSettingUtils {
final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
// do nothing.
}
-
- public static int[] getAdditionalNativeSuggestOptions() {
- return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
- }
}
diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
index b1cd88729..1b592b565 100644
--- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
@@ -39,6 +39,8 @@ public final class DebugSettings extends PreferenceFragment
public static final String PREF_STATISTICS_LOGGING = "enable_logging";
public static final String PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG =
"use_only_personalization_dictionary_for_debug";
+ public static final String PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG =
+ "boost_personalization_dictionary_for_debug";
private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary";
private static final boolean SHOW_STATISTICS_LOGGING = false;
diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
index 878c505bd..cd726c969 100644
--- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
+++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
@@ -34,6 +34,9 @@ public class NativeSuggestOptions {
}
public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+ if (additionalOptions == null) {
+ return;
+ }
for (int i = 0; i < additionalOptions.length; i++) {
setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
}
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index d432087d3..fd83865ba 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -27,10 +27,10 @@ import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
-import com.android.inputmethod.latin.utils.DebugLogUtils;
import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.RunInLocale;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.HashMap;
import java.util.Locale;
@@ -90,6 +90,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
"pref_suppress_language_switch_key";
+ private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
+ "pref_last_used_personalization_token";
public static final String PREF_SEND_FEEDBACK = "send_feedback";
public static final String PREF_ABOUT_KEYBOARD = "about_keyboard";
@@ -343,4 +345,20 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(
DebugSettings.PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false);
}
+
+ public static boolean readBoostPersonalizationDictionaryForDebug(
+ final SharedPreferences prefs) {
+ return prefs.getBoolean(
+ DebugSettings.PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false);
+ }
+
+ public void writeLastUsedPersonalizationToken(byte[] token) {
+ final String tokenStr = StringUtils.byteArrayToHexString(token);
+ mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply();
+ }
+
+ public byte[] readLastUsedPersonalizationToken() {
+ final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null);
+ return StringUtils.hexStringToByteArray(tokenStr);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
index 446777704..1677e1828 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
@@ -229,10 +229,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment
if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen());
- } else {
- AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
}
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
+
setupKeyLongpressTimeoutSettings(prefs, res);
setupKeypressVibrationDurationSettings(prefs, res);
setupKeypressSoundVolumeSettings(prefs, res);
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index a25cf620c..32730d23f 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -22,6 +22,7 @@ import android.content.res.Resources;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.internal.KeySpecParser;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.InputAttributes;
@@ -56,6 +57,7 @@ public final class SettingsValues {
public final SuggestedWords mSuggestPuncList;
public final String mWordSeparators;
public final CharSequence mHintToSaveText;
+ public final boolean mCurrentLanguageHasSpaces;
// From preferences, in the same order as xml/prefs.xml:
public final boolean mAutoCap;
@@ -90,6 +92,8 @@ public final class SettingsValues {
public final int mSuggestionVisibility;
private final boolean mVoiceKeyEnabled;
private final boolean mVoiceKeyOnMain;
+ public final boolean mBoostPersonalizationDictionaryForDebug;
+ public final boolean mUseOnlyPersonalizationDictionaryForDebug;
// Setting values for additional features
public final int[] mAdditionalFeaturesSettingValues =
@@ -117,6 +121,7 @@ public final class SettingsValues {
mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
mWordSeparators = res.getString(R.string.symbols_word_separators);
mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
+ mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
// Store the input attributes
if (null == inputAttributes) {
@@ -168,6 +173,62 @@ public final class SettingsValues {
AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
prefs, mAdditionalFeaturesSettingValues);
mIsInternal = Settings.isInternal(prefs);
+ mBoostPersonalizationDictionaryForDebug =
+ Settings.readBoostPersonalizationDictionaryForDebug(prefs);
+ mUseOnlyPersonalizationDictionaryForDebug =
+ Settings.readUseOnlyPersonalizationDictionaryForDebug(prefs);
+ }
+
+ // Only for tests
+ private SettingsValues(final Locale locale) {
+ // TODO: locale is saved, but not used yet. May have to change this if tests require.
+ mLocale = locale;
+ mDelayUpdateOldSuggestions = 0;
+ mSymbolsPrecededBySpace = new int[] { '(', '[', '{', '&' };
+ Arrays.sort(mSymbolsPrecededBySpace);
+ mSymbolsFollowedBySpace = new int[] { '.', ',', ';', ':', '!', '?', ')', ']', '}', '&' };
+ Arrays.sort(mSymbolsFollowedBySpace);
+ mWordConnectors = new int[] { '\'', '-' };
+ Arrays.sort(mWordConnectors);
+ final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" };
+ mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
+ mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\"";
+ mHintToSaveText = "Touch again to save";
+ mCurrentLanguageHasSpaces = true;
+ mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+ mAutoCap = true;
+ mVibrateOn = true;
+ mSoundOn = true;
+ mKeyPreviewPopupOn = true;
+ mSlidingKeyInputPreviewEnabled = true;
+ mVoiceMode = "0";
+ mIncludesOtherImesInLanguageSwitchList = false;
+ mShowsLanguageSwitchKey = true;
+ mUseContactsDict = true;
+ mUseDoubleSpacePeriod = true;
+ mBlockPotentiallyOffensive = true;
+ mAutoCorrectEnabled = true;
+ mBigramPredictionEnabled = true;
+ mKeyLongpressTimeout = 300;
+ mKeypressVibrationDuration = 5;
+ mKeypressSoundVolume = 1;
+ mKeyPreviewPopupDismissDelay = 70;
+ mAutoCorrectionThreshold = 1;
+ mVoiceKeyEnabled = true;
+ mVoiceKeyOnMain = true;
+ mGestureInputEnabled = true;
+ mGestureTrailEnabled = true;
+ mGestureFloatingPreviewTextEnabled = true;
+ mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+ mSuggestionVisibility = 0;
+ mIsInternal = false;
+ mBoostPersonalizationDictionaryForDebug = false;
+ mUseOnlyPersonalizationDictionaryForDebug = false;
+ }
+
+ @UsedForTesting
+ public static SettingsValues makeDummySettingsValuesForTest(final Locale locale) {
+ return new SettingsValues(locale);
}
public boolean isApplicationSpecifiedCompletionsOn() {
@@ -194,6 +255,10 @@ public final class SettingsValues {
return Arrays.binarySearch(mWordConnectors, code) >= 0;
}
+ public boolean isWordCodePoint(final int code) {
+ return Character.isLetter(code) || isWordConnector(code);
+ }
+
public boolean isUsuallyPrecededBySpace(final int code) {
return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0;
}
@@ -241,7 +306,8 @@ public final class SettingsValues {
// TODO: Stop using KeySpceParser.getLabel().
puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
- Dictionary.TYPE_HARDCODED));
+ Dictionary.DICTIONARY_HARDCODED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */));
}
}
return new SuggestedWords(puncList,
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 6719e98da..69f9a467f 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -301,12 +301,14 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
// TODO: make a spell checker option to block offensive words or not
final ArrayList<SuggestedWordInfo> suggestions =
dictInfo.mDictionary.getSuggestions(composer, prevWord,
- dictInfo.getProximityInfo(),
- true /* blockOffensiveWords */);
- for (final SuggestedWordInfo suggestion : suggestions) {
- final String suggestionStr = suggestion.mWord;
- suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
- suggestionStr.length(), suggestion.mScore);
+ dictInfo.getProximityInfo(), true /* blockOffensiveWords */,
+ null /* additionalFeaturesOptions */);
+ if (suggestions != null) {
+ for (final SuggestedWordInfo suggestion : suggestions) {
+ final String suggestionStr = suggestion.mWord;
+ suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+ suggestionStr.length(), suggestion.mScore);
+ }
}
isInDict = isInDictForAnyCapitalization(dictInfo.mDictionary, text, capitalizeType);
} finally {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index ac8f68781..a0aed2829 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -52,7 +52,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
return noSuggestions;
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index e97069dff..acd47450b 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -210,7 +210,8 @@ public final class MoreSuggestions extends Keyboard {
final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE;
final Key key = new Key(
params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions,
- null, x, y, width, params.mDefaultRowHeight, 0);
+ null /* outputText */, x, y, width, params.mDefaultRowHeight,
+ 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL);
params.markAsEdgeKey(key, index);
params.onAddKey(key);
final int columnNumber = params.getColumnNumber(index);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index d585b5c7f..0ebe37782 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -34,7 +34,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
private static final String TAG = MoreSuggestionsView.class.getSimpleName();
public MoreSuggestionsView(final Context context, final AttributeSet attrs) {
- this(context, attrs, R.attr.moreSuggestionsViewStyle);
+ this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
}
public MoreSuggestionsView(final Context context, final AttributeSet attrs,
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 1dd04fc4d..8d2689a7d 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -122,27 +122,15 @@ final class SuggestionStripLayoutHelper {
mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+ R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView);
mSuggestionStripOption = a.getInt(
R.styleable.SuggestionStripView_suggestionStripOption, 0);
- final float alphaValidTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
- final float alphaTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
- final float alphaAutoCorrect = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
- final float alphaSuggested = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
mAlphaObsoleted = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
- mColorValidTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
- mColorTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
- mColorAutoCorrect = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
- mColorSuggested = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
+ R.styleable.SuggestionStripView_alphaObsoleted, 1.0f);
+ mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0);
+ mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0);
+ mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0);
+ mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0);
mSuggestionsCountInStrip = a.getInt(
R.styleable.SuggestionStripView_suggestionsCountInStrip,
DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index a8a14a825..75f17c559 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -198,7 +198,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public boolean onLongClick(final View view) {
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
Constants.NOT_A_CODE, this);
return showMoreSuggestions();
}
diff --git a/java/src/com/android/inputmethod/latin/utils/ByteArrayWrapper.java b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
index 1bb27aa2b..2028298f2 100644
--- a/java/src/com/android/inputmethod/latin/utils/ByteArrayWrapper.java
+++ b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
@@ -16,17 +16,17 @@
package com.android.inputmethod.latin.utils;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
/**
* This class provides an implementation for the FusionDictionary buffer interface that is backed
* by a simpled byte array. It allows to create a binary dictionary in memory.
*/
-public final class ByteArrayWrapper implements FusionDictionaryBufferInterface {
+public final class ByteArrayDictBuffer implements DictBuffer {
private byte[] mBuffer;
private int mPosition;
- public ByteArrayWrapper(final byte[] buffer) {
+ public ByteArrayDictBuffer(final byte[] buffer) {
mBuffer = buffer;
mPosition = 0;
}
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index 98f0d8b68..cc25102ce 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils;
import android.util.SparseArray;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -94,6 +95,10 @@ public final class CollectionUtils {
return new CopyOnWriteArrayList<E>(array);
}
+ public static <E> ArrayDeque<E> newArrayDeque() {
+ return new ArrayDeque<E>();
+ }
+
public static <E> SparseArray<E> newSparseArray() {
return new SparseArray<E>();
}
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 34eccd65b..021bf0825 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -27,10 +27,8 @@ import com.android.inputmethod.latin.BinaryDictionaryGetter;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import java.io.File;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
@@ -281,13 +279,7 @@ public class DictionaryInfoUtils {
}
public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
- try {
- return BinaryDictIOUtils.getDictionaryFileHeader(file, 0, file.length());
- } catch (UnsupportedFormatException e) {
- return null;
- } catch (IOException e) {
- return null;
- }
+ return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length());
}
private static DictionaryInfo createDictionaryInfoFromFileAddress(
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 7406d855a..be4184093 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -18,7 +18,9 @@ package com.android.inputmethod.latin.utils;
import android.text.TextUtils;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.settings.SettingsValues;
import java.util.ArrayList;
import java.util.Locale;
@@ -193,27 +195,56 @@ public final class StringUtils {
}
public static boolean isIdenticalAfterUpcase(final String text) {
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
final int codePoint = text.codePointAt(i);
if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
return false;
}
+ i += Character.charCount(codePoint);
}
return true;
}
public static boolean isIdenticalAfterDowncase(final String text) {
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
final int codePoint = text.codePointAt(i);
if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
return false;
}
+ i += Character.charCount(codePoint);
}
return true;
}
+ @UsedForTesting
+ public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+ final SettingsValues settings) {
+ if (TextUtils.isEmpty(text)) return false;
+ final int length = text.length();
+ int i = 0;
+ int digitCount = 0;
+ while (i < length) {
+ final int codePoint = Character.codePointAt(text, i);
+ final int charCount = Character.charCount(codePoint);
+ i += charCount;
+ if (Character.isDigit(codePoint)) {
+ // Count digits: see below
+ digitCount += charCount;
+ continue;
+ }
+ if (!settings.isWordCodePoint(codePoint)) return false;
+ }
+ // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+ // card numbers. It would come in handy for word prediction though; a good example is
+ // when writing one's address where the street number is usually quite discriminative,
+ // as well as the postal code.
+ return digitCount < length;
+ }
+
public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
final String separators) {
boolean needCapsNext = true;
@@ -316,4 +347,47 @@ public final class StringUtils {
// Otherwise, it doesn't look like an URL.
return false;
}
+
+ public static boolean isEmptyStringOrWhiteSpaces(String s) {
+ final int N = codePointCount(s);
+ for (int i = 0; i < N; ++i) {
+ if (!Character.isWhitespace(s.codePointAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @UsedForTesting
+ public static String byteArrayToHexString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b & 0xff));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Convert hex string to byte array. The string length must be an even number.
+ */
+ @UsedForTesting
+ public static byte[] hexStringToByteArray(String hexString) {
+ if (TextUtils.isEmpty(hexString)) {
+ return null;
+ }
+ final int N = hexString.length();
+ if (N % 2 != 0) {
+ throw new NumberFormatException("Input hex string length must be an even number."
+ + " Length = " + N);
+ }
+ final byte[] bytes = new byte[N / 2];
+ for (int i = 0; i < N; i += 2) {
+ bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ + Character.digit(hexString.charAt(i + 1), 16));
+ }
+ return bytes;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 16728092d..102a41b4e 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -40,6 +40,7 @@ public final class SubtypeLocaleUtils {
// Special language code to represent "no language".
public static final String NO_LANGUAGE = "zz";
public static final String QWERTY = "qwerty";
+ public static final String EMOJI = "emoji";
public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
private static boolean sInitialized = false;
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index d02f7187e..99788f6f2 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -20,20 +20,20 @@ import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.DictEncoder;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
import com.android.inputmethod.latin.makedict.PendingAttribute;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.makedict.Ver3DictDecoder;
import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList;
import java.io.IOException;
-import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
/**
* Reads and writes Binary files for a UserHistoryDictionary.
@@ -57,12 +57,12 @@ public final class UserHistoryDictIOUtils {
/**
* Writes dictionary to file.
*/
- public static void writeDictionaryBinary(final OutputStream destination,
+ public static void writeDictionary(final DictEncoder dictEncoder,
final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
final FormatOptions formatOptions) {
final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams);
try {
- BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions);
+ dictEncoder.writeDictionary(fusionDict, formatOptions);
Log.d(TAG, "end writing");
} catch (IOException e) {
Log.e(TAG, "IO exception while writing file", e);
@@ -77,7 +77,7 @@ public final class UserHistoryDictIOUtils {
@UsedForTesting
static FusionDictionary constructFusionDictionary(
final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
- final FusionDictionary fusionDict = new FusionDictionary(new Node(),
+ final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(),
new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
false));
int profTotal = 0;
@@ -101,7 +101,7 @@ public final class UserHistoryDictIOUtils {
if (word1 == null) { // unigram
fusionDict.add(word2, freq, null, false /* isNotAWord */);
} else { // bigram
- if (FusionDictionary.findWordInTree(fusionDict.mRoot, word1) == null) {
+ if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) {
fusionDict.add(word1, 2, null, false /* isNotAWord */);
}
fusionDict.setBigram(word1, word2, freq);
@@ -118,14 +118,13 @@ public final class UserHistoryDictIOUtils {
/**
* Reads dictionary from file.
*/
- public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer,
+ public static void readDictionaryBinary(final Ver3DictDecoder dictDecoder,
final OnAddWordListener dict) {
- final Map<Integer, String> unigrams = CollectionUtils.newTreeMap();
- final Map<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
- final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
try {
- BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies,
- bigrams);
+ dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams);
} catch (IOException e) {
Log.e(TAG, "IO exception while reading file", e);
} catch (UnsupportedFormatException e) {
@@ -140,10 +139,11 @@ public final class UserHistoryDictIOUtils {
* Adds all unigrams and bigrams in maps to OnAddWordListener.
*/
@UsedForTesting
- static void addWordsFromWordMap(final Map<Integer, String> unigrams,
- final Map<Integer, Integer> frequencies,
- final Map<Integer, ArrayList<PendingAttribute>> bigrams, final OnAddWordListener to) {
- for (Map.Entry<Integer, String> entry : unigrams.entrySet()) {
+ static void addWordsFromWordMap(final TreeMap<Integer, String> unigrams,
+ final TreeMap<Integer, Integer> frequencies,
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams,
+ final OnAddWordListener to) {
+ for (Entry<Integer, String> entry : unigrams.entrySet()) {
final String word1 = entry.getValue();
final int unigramFrequency = frequencies.get(entry.getKey());
to.setUnigram(word1, null, unigramFrequency);
@@ -156,7 +156,7 @@ public final class UserHistoryDictIOUtils {
continue;
}
to.setBigram(word1, word2,
- BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency,
+ BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency,
attr.mFrequency));
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
index 713a45bda..1992b2f5d 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
@@ -23,7 +23,9 @@ import java.util.concurrent.TimeUnit;
public final class UserHistoryForgettingCurveUtils {
private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
private static final boolean DEBUG = false;
- private static final int FC_FREQ_MAX = 127;
+ private static final int DEFAULT_FC_FREQ = 127;
+ private static final int BOOSTED_FC_FREQ = 200;
+ private static int FC_FREQ_MAX = DEFAULT_FC_FREQ;
/* package */ static final int COUNT_MAX = 3;
private static final int FC_LEVEL_MAX = 3;
/* package */ static final int ELAPSED_TIME_MAX = 15;
@@ -33,6 +35,14 @@ public final class UserHistoryForgettingCurveUtils {
private static final int HALF_LIFE_HOURS = 48;
private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1);
+ public static void boostMaxFreqForDebug() {
+ FC_FREQ_MAX = BOOSTED_FC_FREQ;
+ }
+
+ public static void resetMaxFreqForDebug() {
+ FC_FREQ_MAX = DEFAULT_FC_FREQ;
+ }
+
private UserHistoryForgettingCurveUtils() {
// This utility class is not publicly instantiable.
}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
index 161386e2e..a75d353c9 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
@@ -19,6 +19,7 @@ package com.android.inputmethod.latin.utils;
import android.inputmethodservice.InputMethodService;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.settings.Settings;
public final class UserLogRingCharBuffer {
@@ -64,6 +65,9 @@ public final class UserLogRingCharBuffer {
if (!mEnabled) {
return;
}
+ if (LatinImeLogger.sUsabilityStudy) {
+ UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
+ }
mCharBuf[mEnd] = c;
mXBuf[mEnd] = x;
mYBuf[mEnd] = y;
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
index 63d524df7..2beebdfae 100644
--- a/java/src/com/android/inputmethod/research/JsonUtils.java
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -75,12 +75,12 @@ import java.util.Map;
private static void writeJson(final Key key, final JsonWriter jsonWriter) throws IOException {
jsonWriter.beginObject();
- jsonWriter.name("code").value(key.mCode);
+ jsonWriter.name("code").value(key.getCode());
jsonWriter.name("altCode").value(key.getAltCode());
- jsonWriter.name("x").value(key.mX);
- jsonWriter.name("y").value(key.mY);
- jsonWriter.name("w").value(key.mWidth);
- jsonWriter.name("h").value(key.mHeight);
+ jsonWriter.name("x").value(key.getX());
+ jsonWriter.name("y").value(key.getY());
+ jsonWriter.name("w").value(key.getWidth());
+ jsonWriter.name("h").value(key.getHeight());
jsonWriter.endObject();
}
@@ -103,7 +103,7 @@ import java.util.Map;
jsonWriter.name("word").value(wordInfo.toString());
jsonWriter.name("score").value(wordInfo.mScore);
jsonWriter.name("kind").value(wordInfo.mKind);
- jsonWriter.name("sourceDict").value(wordInfo.mSourceDict);
+ jsonWriter.name("sourceDict").value(wordInfo.mSourceDict.mDictType);
jsonWriter.endObject();
}
jsonWriter.endArray();
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 3a3408266..da9c61103 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -1427,7 +1427,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
- keyboard.mOccupiedHeight, keyboard.mKeys);
+ keyboard.mOccupiedHeight, keyboard.getKeys());
}
/**