diff options
Diffstat (limited to 'java/src')
34 files changed, 1389 insertions, 481 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java index 25ff8d0ce..702ed2075 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java @@ -19,12 +19,18 @@ package com.android.inputmethod.keyboard; import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Rect; +import android.os.Build; +import android.preference.PreferenceManager; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.util.AttributeSet; +import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -35,16 +41,21 @@ 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.DynamicGridKeyboard; 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.settings.Settings; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.ResourceUtils; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; /** * View class to implement Emoji keyboards. @@ -60,51 +71,287 @@ import java.util.HashMap; public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener, ViewPager.OnPageChangeListener, View.OnClickListener, ScrollKeyboardView.OnKeyClickListener { + private static final String TAG = EmojiKeyboardView.class.getSimpleName(); private final int mKeyBackgroundId; + private final int mEmojiFunctionalKeyBackgroundId; + private final KeyboardLayoutSet mLayoutSet; private final ColorStateList mTabLabelColor; - private final EmojiKeyboardAdapter mEmojiKeyboardAdapter; + private 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, - }; + private static final int CATEGORY_ID_UNSPECIFIED = -1; + public static final int CATEGORY_ID_RECENTS = 0; + public static final int CATEGORY_ID_PEOPLE = 1; + public static final int CATEGORY_ID_OBJECTS = 2; + public static final int CATEGORY_ID_NATURE = 3; + public static final int CATEGORY_ID_PLACES = 4; + public static final int CATEGORY_ID_SYMBOLS = 5; + public static final int CATEGORY_ID_EMOTICONS = 6; + + private static class CategoryProperties { + public int mCategoryId; + public int mPageCount; + public CategoryProperties(final int categoryId, final int pageCount) { + mCategoryId = categoryId; + mPageCount = pageCount; + } + } + + private static class EmojiCategory { + 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 }; + private final SharedPreferences mPrefs; + private final int mMaxPageKeyCount; + private final KeyboardLayoutSet mLayoutSet; + private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap(); + private final ArrayList<CategoryProperties> mShownCategories = + CollectionUtils.newArrayList(); + private final ConcurrentHashMap<Long, DynamicGridKeyboard> + mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>(); + + private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED; + private int mCurrentCategoryPageId = 0; + + public EmojiCategory(final SharedPreferences prefs, final Resources res, + final KeyboardLayoutSet layoutSet) { + mPrefs = prefs; + mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count); + mLayoutSet = layoutSet; + for (int i = 0; i < sCategoryName.length; ++i) { + mCategoryNameToIdMap.put(sCategoryName[i], i); + } + addShownCategoryId(CATEGORY_ID_RECENTS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + addShownCategoryId(CATEGORY_ID_PEOPLE); + addShownCategoryId(CATEGORY_ID_OBJECTS); + addShownCategoryId(CATEGORY_ID_NATURE); + addShownCategoryId(CATEGORY_ID_PLACES); + mCurrentCategoryId = CATEGORY_ID_PEOPLE; + } else { + mCurrentCategoryId = CATEGORY_ID_SYMBOLS; + } + addShownCategoryId(CATEGORY_ID_SYMBOLS); + addShownCategoryId(CATEGORY_ID_EMOTICONS); + getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */) + .loadRecentKeys(mCategoryKeyboardMap.values()); + } + + private void addShownCategoryId(int categoryId) { + // Load a keyboard of categoryId + getKeyboard(categoryId, 0 /* cagetoryPageId */); + final CategoryProperties properties = + new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); + mShownCategories.add(properties); + } + + public String getCategoryName(int categoryId, int categoryPageId) { + return sCategoryName[categoryId] + "-" + categoryPageId; + } + + public int getCategoryId(String name) { + final String[] strings = name.split("-"); + return mCategoryNameToIdMap.get(strings[0]); + } + + public int getCategoryIcon(int categoryId) { + return sCategoryIcon[categoryId]; + } + + public String getCategoryLabel(int categoryId) { + return sCategoryLabel[categoryId]; + } + + public ArrayList<CategoryProperties> getShownCategories() { + return mShownCategories; + } + + public int getCurrentCategoryId() { + return mCurrentCategoryId; + } + + public void setCurrentCategoryId(int categoryId) { + mCurrentCategoryId = categoryId; + } + + public void setCurrentCategoryPageId(int id) { + mCurrentCategoryPageId = id; + } + + public void saveLastTypedCategoryPage() { + Settings.writeEmojiCategoryLastTypedId( + mPrefs, mCurrentCategoryId, mCurrentCategoryPageId); + } + + public boolean isInRecentTab() { + return mCurrentCategoryId == CATEGORY_ID_RECENTS; + } + + public int getTabIdFromCategoryId(int categoryId) { + for (int i = 0; i < mShownCategories.size(); ++i) { + if (mShownCategories.get(i).mCategoryId == categoryId) { + return i; + } + } + Log.w(TAG, "categoryId not found: " + categoryId); + return 0; + } + + // Returns the view pager's page position for the categoryId + public int getPageIdFromCategoryId(int categoryId) { + final int lastSavedCategoryPageId = + Settings.readEmojiCategoryLastTypedId(mPrefs, categoryId); + int sum = 0; + for (int i = 0; i < mShownCategories.size(); ++i) { + final CategoryProperties props = mShownCategories.get(i); + if (props.mCategoryId == categoryId) { + return sum + lastSavedCategoryPageId; + } + sum += props.mPageCount; + } + Log.w(TAG, "categoryId not found: " + categoryId); + return 0; + } + + public int getRecentTabId() { + return getTabIdFromCategoryId(CATEGORY_ID_RECENTS); + } + + private int getCategoryPageCount(int categoryId) { + final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); + return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1; + } + + // Returns a pair of the category id and the category page id from the view pager's page + // position. The category page id is numbered in each category. And the view page position + // is the position of the current shown page in the view pager which contains all pages of + // all categories. + public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) { + int sum = 0; + for (CategoryProperties properties : mShownCategories) { + final int temp = sum; + sum += properties.mPageCount; + if (sum > position) { + return new Pair<Integer, Integer>(properties.mCategoryId, position - temp); + } + } + return null; + } + + // Returns a keyboard from the view pager's page position. + public DynamicGridKeyboard getKeyboardFromPagePosition(int position) { + final Pair<Integer, Integer> categoryAndId = + getCategoryIdAndPageIdFromPagePosition(position); + if (categoryAndId != null) { + return getKeyboard(categoryAndId.first, categoryAndId.second); + } + return null; + } + + public DynamicGridKeyboard getKeyboard(int categoryId, int id) { + synchronized(mCategoryKeyboardMap) { + final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; + final DynamicGridKeyboard kbd; + if (!mCategoryKeyboardMap.containsKey(key)) { + if (categoryId != CATEGORY_ID_RECENTS) { + final Keyboard keyboard = + mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); + final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount); + for (int i = 0; i < sortedKeys.length; ++i) { + final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId, i /* categoryPageId */); + for (Key emojiKey : sortedKeys[i]) { + if (emojiKey == null) { + break; + } + tempKbd.addKeyLast(emojiKey); + } + mCategoryKeyboardMap.put((((long) categoryId) + << Constants.MAX_INT_BIT_COUNT) | i, tempKbd); + } + kbd = mCategoryKeyboardMap.get(key); + } else { + kbd = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId, 0 /* categoryPageId */); + mCategoryKeyboardMap.put(key, kbd); + } + } else { + kbd = mCategoryKeyboardMap.get(key); + } + return kbd; + } + } + + public int getTotalPageCountOfAllCategories() { + int sum = 0; + for (CategoryProperties properties : mShownCategories) { + sum += properties.mPageCount; + } + return sum; + } + + private Key[][] sortKeys(Key[] inKeys, int maxPageCount) { + Key[] keys = Arrays.copyOf(inKeys, inKeys.length); + Arrays.sort(keys, 0, keys.length, new Comparator<Key>() { + @Override + public int compare(Key lhs, Key rhs) { + final Rect lHitBox = lhs.getHitBox(); + final Rect rHitBox = rhs.getHitBox(); + if (lHitBox.top < rHitBox.top) { + return -1; + } else if (lHitBox.top > rHitBox.top) { + return 1; + } + if (lHitBox.left < rHitBox.left) { + return -1; + } else if (lHitBox.left > rHitBox.left) { + return 1; + } + if (lhs.getCode() == rhs.getCode()) { + return 0; + } + return lhs.getCode() < rhs.getCode() ? -1 : 1; + } + }); + final int pageCount = (keys.length - 1) / maxPageCount + 1; + final Key[][] retval = new Key[pageCount][maxPageCount]; + for (int i = 0; i < keys.length; ++i) { + retval[i / maxPageCount][i % maxPageCount] = keys[i]; + } + return retval; + } + } + + private final EmojiCategory mEmojiCategory; public EmojiKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.emojiKeyboardViewStyle); @@ -116,6 +363,8 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange R.styleable.KeyboardView, defStyle, R.style.KeyboardView); mKeyBackgroundId = keyboardViewAttr.getResourceId( R.styleable.KeyboardView_keyBackground, 0); + mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId( + R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0); keyboardViewAttr.recycle(); final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs, R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView); @@ -125,15 +374,14 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( context, null /* editorInfo */); final Resources res = context.getResources(); + final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); 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))); + emojiLp.mEmojiKeyboardHeight); builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */); - final KeyboardLayoutSet layoutSet = builder.build(); - mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(layoutSet, this); - // TODO: Save/restore recent keys from/to preferences. + mLayoutSet = builder.build(); + mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), + context.getResources(), builder.build()); } @Override @@ -149,23 +397,21 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange setMeasuredDimension(width, height); } - private void addTab(final TabHost host, final int category) { - final String tabId = sCategoryName[category]; - sCategoryNameToIdMap.put(tabId, category); + private void addTab(final TabHost host, final int categoryId) { + final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */); final TabHost.TabSpec tspec = host.newTabSpec(tabId); tspec.setContent(R.id.emoji_keyboard_dummy); - if (sCategoryIcon[category] != 0) { + if (mEmojiCategory.getCategoryIcon(categoryId) != 0) { final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( R.layout.emoji_keyboard_tab_icon, null); - iconView.setImageResource(sCategoryIcon[category]); + iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId)); tspec.setIndicator(iconView); } - if (sCategoryLabel[category] != null) { + if (mEmojiCategory.getCategoryLabel(categoryId) != null) { final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate( R.layout.emoji_keyboard_tab_label, null); - textView.setText(sCategoryLabel[category]); + textView.setText(mEmojiCategory.getCategoryLabel(categoryId)); textView.setTextColor(mTabLabelColor); - textView.setBackgroundResource(mKeyBackgroundId); tspec.setIndicator(textView); } host.addTab(tspec); @@ -175,55 +421,60 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange 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); + for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) { + addTab(mTabHost, properties.mCategoryId); + } mTabHost.setOnTabChangedListener(this); mTabHost.getTabWidget().setStripEnabled(true); + mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(mEmojiCategory, mLayoutSet, this); + 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); + final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); + emojiLp.setPagerProps(mEmojiPager); + + setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); - // TODO: Record current category. - final int category = CATEGORY_PEOPLE; - setCurrentCategory(category, true /* force */); + final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); + emojiLp.setActionBarProps(actionBar); // TODO: Implement auto repeat, using View.OnTouchListener? - final View deleteKey = findViewById(R.id.emoji_keyboard_delete); - deleteKey.setBackgroundResource(mKeyBackgroundId); + final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete); + deleteKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); deleteKey.setTag(Constants.CODE_DELETE); deleteKey.setOnClickListener(this); - final View alphabetKey = findViewById(R.id.emoji_keyboard_alphabet); - alphabetKey.setBackgroundResource(mKeyBackgroundId); + final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet); + alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); alphabetKey.setOnClickListener(this); - final View sendKey = findViewById(R.id.emoji_keyboard_send); - sendKey.setBackgroundResource(mKeyBackgroundId); + final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space); + spaceKey.setBackgroundResource(mKeyBackgroundId); + spaceKey.setTag(Constants.CODE_SPACE); + spaceKey.setOnClickListener(this); + emojiLp.setKeyProps(spaceKey); + final ImageView sendKey = (ImageView)findViewById(R.id.emoji_keyboard_send); + sendKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); 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 */); + final int categoryId = mEmojiCategory.getCategoryId(tabId); + setCurrentCategoryId(categoryId, false /* force */); } @Override public void onPageSelected(final int position) { - setCurrentCategory(position, false /* force */); + final Pair<Integer, Integer> newPos = + mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); + setCurrentCategoryId(newPos.first /* categoryId */, false /* force */); + mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */); } @Override @@ -254,7 +505,11 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange @Override public void onKeyClick(final Key key) { - mEmojiKeyboardAdapter.addRecentKey(key); + // TODO: Save emoticons to recents + if (mEmojiCategory.getCurrentCategoryId() != CATEGORY_ID_EMOTICONS) { + mEmojiKeyboardAdapter.addRecentKey(key); + } + mEmojiCategory.saveLastTypedCategoryPage(); final int code = key.getCode(); if (code == Constants.CODE_OUTPUT_TEXT) { mKeyboardActionListener.onTextInput(key.getOutputText()); @@ -271,43 +526,46 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange mKeyboardActionListener = listener; } - private void setCurrentCategory(final int category, final boolean force) { - if (mCurrentCategory == category && !force) { + private void setCurrentCategoryId(final int categoryId, final boolean force) { + if (mEmojiCategory.getCurrentCategoryId() == categoryId && !force) { return; } - mCurrentCategory = category; - if (force || mEmojiPager.getCurrentItem() != category) { - mEmojiPager.setCurrentItem(category, true /* smoothScroll */); + mEmojiCategory.setCurrentCategoryId(categoryId); + final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); + final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId); + if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( + mEmojiPager.getCurrentItem()).first != categoryId) { + mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */); } - if (force || mTabHost.getCurrentTab() != category) { - mTabHost.setCurrentTab(category); + if (force || mTabHost.getCurrentTab() != newTabId) { + mTabHost.setCurrentTab(newTabId); } - // 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 DynamicGridKeyboard mRecentsKeyboard; private final SparseArray<ScrollKeyboardView> mActiveKeyboardView = CollectionUtils.newSparseArray(); - private int mActivePosition = CATEGORY_UNSPECIFIED; + private final EmojiCategory mEmojiCategory; + private int mActivePosition = 0; - public EmojiKeyboardAdapter(final KeyboardLayoutSet layoutSet, + public EmojiKeyboardAdapter(final EmojiCategory emojiCategory, + final KeyboardLayoutSet layoutSet, final ScrollKeyboardView.OnKeyClickListener listener) { + mEmojiCategory = emojiCategory; mListener = listener; - mLayoutSet = layoutSet; - mRecentsKeyboard = new RecentsKeyboard( - layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS)); + mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0); } public void addRecentKey(final Key key) { - if (mActivePosition == CATEGORY_RECENTS) { + if (mEmojiCategory.isInRecentTab()) { return; } - mRecentsKeyboard.addRecentKey(key); - final KeyboardView recentKeyboardView = mActiveKeyboardView.get(CATEGORY_RECENTS); + mRecentsKeyboard.addKeyFirst(key); + final KeyboardView recentKeyboardView = + mActiveKeyboardView.get(mEmojiCategory.getRecentTabId()); if (recentKeyboardView != null) { recentKeyboardView.invalidateAllKeys(); } @@ -315,7 +573,7 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange @Override public int getCount() { - return sCategoryName.length; + return mEmojiCategory.getTotalPageCountOfAllCategories(); } @Override @@ -333,9 +591,8 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange @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 Keyboard keyboard = + mEmojiCategory.getKeyboardFromPagePosition(position); final LayoutInflater inflater = LayoutInflater.from(container.getContext()); final View view = inflater.inflate( R.layout.emoji_keyboard_page, container, false /* attachToRoot */); diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java new file mode 100644 index 000000000..5570d594d --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java @@ -0,0 +1,80 @@ +/* + * 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 com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.ResourceUtils; + +import android.content.res.Resources; +import android.support.v4.view.ViewPager; +import android.widget.ImageView; +import android.widget.LinearLayout; + +public class EmojiLayoutParams { + private static final int DEFAULT_KEYBOARD_ROWS = 4; + + public final int mEmojiPagerHeight; + private final int mEmojiPagerBottomMargin; + public final int mEmojiKeyboardHeight; + public final int mEmojiActionBarHeight; + public final int mKeyVerticalGap; + private final int mKeyHorizontalGap; + private final int mBottomPadding; + private final int mTopPadding; + + public EmojiLayoutParams(Resources res) { + final int defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); + final int defaultKeyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); + mKeyVerticalGap = (int) res.getFraction(R.fraction.key_bottom_gap_ics, + (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); + mBottomPadding = (int) res.getFraction(R.fraction.keyboard_bottom_padding_ics, + (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); + mTopPadding = (int) res.getFraction(R.fraction.keyboard_top_padding_ics, + (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); + mKeyHorizontalGap = (int) (res.getFraction(R.fraction.key_horizontal_gap_ics, + defaultKeyboardWidth, defaultKeyboardWidth)); + final int baseheight = defaultKeyboardHeight - mBottomPadding - mTopPadding + + mKeyVerticalGap; + mEmojiActionBarHeight = ((int) baseheight) / DEFAULT_KEYBOARD_ROWS + - (mKeyVerticalGap - mBottomPadding) / 2; + mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight; + mEmojiPagerBottomMargin = mKeyVerticalGap / 2; + mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1; + } + + public void setPagerProps(ViewPager vp) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams(); + lp.height = mEmojiPagerHeight - mEmojiPagerBottomMargin; + lp.bottomMargin = mEmojiPagerBottomMargin; + vp.setLayoutParams(lp); + } + + public void setActionBarProps(LinearLayout ll) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); + lp.height = mEmojiActionBarHeight; + lp.topMargin = 0; + lp.bottomMargin = mBottomPadding; + ll.setLayoutParams(lp); + } + + public void setKeyProps(ImageView ib) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ib.getLayoutParams(); + lp.leftMargin = mKeyHorizontalGap / 2; + lp.rightMargin = mKeyHorizontalGap / 2; + ib.setLayoutParams(lp); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index d128d3595..cc1ffd183 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -314,15 +314,19 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState()); } + public boolean isShowingEmojiKeyboard() { + return mEmojiKeyboardView.getVisibility() == View.VISIBLE; + } + public boolean isShowingMoreKeysPanel() { - if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) { + if (isShowingEmojiKeyboard()) { return false; } return mKeyboardView.isShowingMoreKeysPanel(); } public View getVisibleKeyboardView() { - if (mEmojiKeyboardView.getVisibility() == View.VISIBLE) { + if (isShowingEmojiKeyboard()) { return mEmojiKeyboardView; } return mKeyboardView; diff --git a/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java index 629c60460..f203eb7d7 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/RecentsKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java @@ -16,33 +16,41 @@ package com.android.inputmethod.keyboard.internal; +import android.content.SharedPreferences; import android.text.TextUtils; +import com.android.inputmethod.keyboard.EmojiKeyboardView; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.ArrayDeque; -import java.util.Random; +import java.util.Collection; /** - * This is a Keyboard class to host recently used keys. + * This is a Keyboard class where you can add keys dynamically shown in a grid layout */ -// TODO: Save/restore recent keys from/to preferences. -public class RecentsKeyboard extends Keyboard { +public class DynamicGridKeyboard extends Keyboard { private static final int TEMPLATE_KEY_CODE_0 = 0x30; private static final int TEMPLATE_KEY_CODE_1 = 0x31; + // Recent codes are saved as an integer array, so we use comma as a separater. + private static final String RECENT_KEY_SEPARATOR = Constants.STRING_COMMA; + private final SharedPreferences mPrefs; 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 final int mMaxKeyCount; + private final boolean mIsRecents; + private final ArrayDeque<GridKey> mGridKeys = CollectionUtils.newArrayDeque(); - private Key[] mCachedRecentKeys; + private Key[] mCachedGridKeys; - public RecentsKeyboard(final Keyboard templateKeyboard) { + public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard, + final int maxKeyCount, final int categoryId, final int categoryPageId) { super(templateKeyboard); final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0); final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1); @@ -50,8 +58,9 @@ public class RecentsKeyboard extends Keyboard { mHorizontalStep = Math.abs(key1.getX() - key0.getX()); mVerticalStep = key0.getHeight() + mVerticalGap; mColumnsNum = mBaseWidth / mHorizontalStep; - final int rowsNum = mBaseHeight / mVerticalStep; - mMaxRecentKeyCount = mColumnsNum * rowsNum; + mMaxKeyCount = maxKeyCount; + mIsRecents = categoryId == EmojiKeyboardView.CATEGORY_ID_RECENTS; + mPrefs = prefs; } private Key getTemplateKey(final int code) { @@ -63,32 +72,67 @@ public class RecentsKeyboard extends Keyboard { throw new RuntimeException("Can't find template key: code=" + code); } - private final Random random = new Random(); + public void addKeyFirst(final Key usedKey) { + addKey(usedKey, true); + if (mIsRecents) { + saveRecentKeys(); + } + } + + public void addKeyLast(final Key usedKey) { + addKey(usedKey, false); + } - 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)) { + private void addKey(final Key usedKey, final boolean addFirst) { + synchronized (mGridKeys) { + mCachedGridKeys = null; + final GridKey key = new GridKey(usedKey); + while (mGridKeys.remove(key)) { // Remove duplicate keys. } - mRecentKeys.addFirst(key); - while (mRecentKeys.size() > mMaxRecentKeyCount) { - mRecentKeys.removeLast(); + if (addFirst) { + mGridKeys.addFirst(key); + } else { + mGridKeys.addLast(key); + } + while (mGridKeys.size() > mMaxKeyCount) { + mGridKeys.removeLast(); } int index = 0; - for (final RecentKey recentKey : mRecentKeys) { + for (final GridKey gridKey : mGridKeys) { 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); + gridKey.updateCorrdinates(keyX, keyY); index++; } } } + private void saveRecentKeys() { + final StringBuilder sb = new StringBuilder(); + for (final Key key : mGridKeys) { + sb.append(key.getCode()).append(RECENT_KEY_SEPARATOR); + } + Settings.writeEmojiRecentKeys(mPrefs, sb.toString()); + } + + public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) { + final String str = Settings.readEmojiRecentKeys(mPrefs); + for (String s : str.split(RECENT_KEY_SEPARATOR)) { + if (TextUtils.isEmpty(s)) { + continue; + } + final int code = Integer.valueOf(s); + for (DynamicGridKeyboard kbd : keyboards) { + final Key key = kbd.getKey(code); + if (key != null) { + addKeyLast(key); + break; + } + } + } + } + private int getKeyX(final int index) { final int column = index % mColumnsNum; return column * mHorizontalStep + mLeftPadding; @@ -101,26 +145,26 @@ public class RecentsKeyboard extends Keyboard { @Override public Key[] getKeys() { - synchronized (mRecentKeys) { - if (mCachedRecentKeys != null) { - return mCachedRecentKeys; + synchronized (mGridKeys) { + if (mCachedGridKeys != null) { + return mCachedGridKeys; } - mCachedRecentKeys = mRecentKeys.toArray(new Key[mRecentKeys.size()]); - return mCachedRecentKeys; + mCachedGridKeys = mGridKeys.toArray(new Key[mGridKeys.size()]); + return mCachedGridKeys; } } @Override public Key[] getNearestKeys(final int x, final int y) { - // TODO: Calculate the nearest key index in mRecentKeys from x and y. + // TODO: Calculate the nearest key index in mGridKeys from x and y. return getKeys(); } - static final class RecentKey extends Key { + static final class GridKey extends Key { private int mCurrentX; private int mCurrentY; - public RecentKey(final Key originalKey) { + public GridKey(final Key originalKey) { super(originalKey); } @@ -151,7 +195,7 @@ public class RecentsKeyboard extends Keyboard { @Override public String toString() { - return "RecentKey: " + super.toString(); + return "GridKey: " + super.toString(); } } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index 7008b0619..a72595f7c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -383,7 +383,7 @@ public final class KeyboardTextsSet { // Label for "switch to more symbol" modifier key. Must be short to fit on key! /* 124 */ "= \\ <", // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 125 */ "~ [ {", + /* 125 */ "~ [ <", // Label for "Tab" key. Must be short to fit on key! /* 126 */ "Tab", // Label for "switch to phone numeric" key. Must be short to fit on key! @@ -2056,6 +2056,25 @@ public final class KeyboardTextsSet { /* 45 */ "\u0410\u0411\u0412", }; + /* Language lo: Lao */ + private static final String[] LANGUAGE_lo = { + /* 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, + /* ~44 */ + // Label for "switch to alphabetic" key. + // U+0E81: "ກ" LAO LETTER KO + // U+0E82: "ຂ" LAO LETTER KHO SUNG + // U+0E84: "ຄ" LAO LETTER KHO TAM + /* 45 */ "\u0E81\u0E82\u0E84", + /* 46~ */ + null, null, null, null, null, + /* ~50 */ + // U+20AD: "₭" KIP SIGN + /* 51 */ "\u20AD", + }; + /* Language lt: Lithuanian */ private static final String[] LANGUAGE_lt = { // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK @@ -2347,6 +2366,63 @@ public final class KeyboardTextsSet { /* 47 */ "!text/double_9qm_rqm", }; + /* Language ne: Nepali */ + private static final String[] LANGUAGE_ne = { + /* 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, + /* ~44 */ + // Label for "switch to alphabetic" key. + // U+0915: "क" DEVANAGARI LETTER KA + // U+0916: "ख" DEVANAGARI LETTER KHA + // U+0917: "ग" DEVANAGARI LETTER GA + /* 45 */ "\u0915\u0916\u0917", + /* 46~ */ + null, null, null, null, null, + /* ~50 */ + // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN + /* 51 */ "\u0930\u0941.", + /* 52~ */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~62 */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* 63 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* 64 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* 65 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* 66 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* 67 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* 68 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* 69 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* 70 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* 71 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* 72 */ "\u0966", + // Label for "switch to symbols" key. + /* 73 */ "?\u0967\u0968\u0969", + // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" + // part because it'll be appended by the code. + /* 74 */ "\u0967\u0968\u0969", + /* 75 */ "1", + /* 76 */ "2", + /* 77 */ "3", + /* 78 */ "4", + /* 79 */ "5", + /* 80 */ "6", + /* 81 */ "7", + /* 82 */ "8", + /* 83 */ "9", + /* 84 */ "0", + }; + /* Language nl: Dutch */ private static final String[] LANGUAGE_nl = { // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE @@ -3332,11 +3408,13 @@ public final class KeyboardTextsSet { "ka", LANGUAGE_ka, /* Georgian */ "kk", LANGUAGE_kk, /* Kazakh */ "ky", LANGUAGE_ky, /* Kirghiz */ + "lo", LANGUAGE_lo, /* Lao */ "lt", LANGUAGE_lt, /* Lithuanian */ "lv", LANGUAGE_lv, /* Latvian */ "mk", LANGUAGE_mk, /* Macedonian */ "mn", LANGUAGE_mn, /* Mongolian */ "nb", LANGUAGE_nb, /* Norwegian Bokmål */ + "ne", LANGUAGE_ne, /* Nepali */ "nl", LANGUAGE_nl, /* Dutch */ "pl", LANGUAGE_pl, /* Polish */ "pt", LANGUAGE_pt, /* Portuguese */ diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index e8b06570f..b49cd80ab 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin; import android.text.TextUtils; import android.util.SparseArray; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.settings.NativeSuggestOptions; @@ -40,14 +41,20 @@ public final class BinaryDictionary extends Dictionary { private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; // Must be equal to MAX_RESULTS in native/jni/src/defines.h private static final int MAX_RESULTS = 18; + // Required space count for auto commit. + // TODO: Remove this heuristic. + private static final int SPACE_COUNT_FOR_AUTO_COMMIT = 3; private long mNativeDict; private final Locale mLocale; + private final long mDictSize; + private final String mDictFilePath; private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS]; private final int[] mSpaceIndices = new int[MAX_RESULTS]; private final int[] mOutputScores = new int[MAX_RESULTS]; private final int[] mOutputTypes = new int[MAX_RESULTS]; + private final int[] mOutputAutoCommitFirstWordConfidence = new int[MAX_RESULTS]; private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); @@ -62,7 +69,7 @@ public final class BinaryDictionary extends Dictionary { if (traverseSession == null) { traverseSession = mDicTraverseSessions.get(traverseSessionId); if (traverseSession == null) { - traverseSession = new DicTraverseSession(mLocale, mNativeDict); + traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize); mDicTraverseSessions.put(traverseSessionId, traverseSession); } } @@ -85,6 +92,8 @@ public final class BinaryDictionary extends Dictionary { final boolean isUpdatable) { super(dictType); mLocale = locale; + mDictSize = length; + mDictFilePath = filename; mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance); loadDictionary(filename, offset, length, isUpdatable); } @@ -95,6 +104,9 @@ public final class BinaryDictionary extends Dictionary { private static native long openNative(String sourceDir, long dictOffset, long dictSize, boolean isUpdatable); + private static native void flushNative(long dict, String filePath); + private static native boolean needsToRunGCNative(long dict); + private static native void flushWithGCNative(long dict, String filePath); private static native void closeNative(long dict); private static native int getProbabilityNative(long dict, int[] word); private static native boolean isValidBigramNative(long dict, int[] word0, int[] word1); @@ -102,7 +114,8 @@ public final class BinaryDictionary extends Dictionary { long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, int[] suggestOptions, int[] prevWordCodePointArray, - int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes); + int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes, + int[] outputAutoCommitFirstWordConfidence); private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); private static native int editDistanceNative(int[] before, int[] after); private static native void addUnigramWordNative(long dict, int[] word, int probability); @@ -155,7 +168,7 @@ public final class BinaryDictionary extends Dictionary { ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(), prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices, - mOutputTypes); + mOutputTypes, mOutputAutoCommitFirstWordConfidence); final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); for (int j = 0; j < count; ++j) { final int start = j * MAX_WORD_LENGTH; @@ -179,7 +192,8 @@ public final class BinaryDictionary extends Dictionary { // flags too and pass mOutputTypes[j] instead of kind suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len), score, kind, this /* sourceDict */, - mSpaceIndices[0] /* indexOfTouchPointOfSecondWord */)); + mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, + mOutputAutoCommitFirstWordConfidence[0])); } } return suggestions; @@ -253,6 +267,40 @@ public final class BinaryDictionary extends Dictionary { removeBigramWordsNative(mNativeDict, codePoints0, codePoints1); } + @UsedForTesting + public void flush() { + if (!isValidDictionary()) return; + flushNative(mNativeDict, mDictFilePath); + } + + @UsedForTesting + public void flushWithGC() { + if (!isValidDictionary()) return; + flushWithGCNative(mNativeDict, mDictFilePath); + } + + @UsedForTesting + public boolean needsToRunGC() { + if (!isValidDictionary()) return false; + return needsToRunGCNative(mNativeDict); + } + + @Override + public boolean shouldAutoCommit(final SuggestedWordInfo candidate) { + // TODO: actually use the confidence rather than use this completely broken heuristic + final String word = candidate.mWord; + final int length = word.length(); + int remainingSpaces = SPACE_COUNT_FOR_AUTO_COMMIT; + for (int i = 0; i < length; ++i) { + // This is okay because no low-surrogate and no high-surrogate can ever match the + // space character, so we don't need to take care of iterating on code points. + if (Constants.CODE_SPACE == word.charAt(i)) { + if (0 >= --remainingSpaces) return true; + } + } + return false; + } + @Override public void close() { synchronized (mDicTraverseSessions) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 2b6d983c0..566184244 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -21,9 +21,10 @@ import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; import android.util.Log; +import com.android.inputmethod.latin.makedict.DictDecoder; +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; @@ -228,7 +229,7 @@ final public class BinaryDictionaryGetter { private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) { try { // Read the version of the file - final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(f); + final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f); final FileHeader header = dictDecoder.readHeader(); final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY); diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 8aec03f71..029ba02ed 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -220,7 +220,11 @@ public final class Constants { } } + public static final int MAX_INT_BIT_COUNT = 32; + public static final String STRING_COMMA = ","; + private Constants() { // This utility class is not publicly instantiable. } + } diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index 45b281318..8d295adee 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -25,16 +25,16 @@ public final class DicTraverseSession { JniUtils.loadNativeLibrary(); } - private static native long setDicTraverseSessionNative(String locale); + private static native long setDicTraverseSessionNative(String locale, long dictSize); private static native void initDicTraverseSessionNative(long nativeDicTraverseSession, long dictionary, int[] previousWord, int previousWordLength); private static native void releaseDicTraverseSessionNative(long nativeDicTraverseSession); private long mNativeDicTraverseSession; - public DicTraverseSession(Locale locale, long dictionary) { + public DicTraverseSession(Locale locale, long dictionary, long dictSize) { mNativeDicTraverseSession = createNativeDicTraverseSession( - locale != null ? locale.toString() : ""); + locale != null ? locale.toString() : "", dictSize); initSession(dictionary); } @@ -51,8 +51,8 @@ public final class DicTraverseSession { mNativeDicTraverseSession, dictionary, previousWord, previousWordLength); } - private final long createNativeDicTraverseSession(String locale) { - return setDicTraverseSessionNative(locale); + private final long createNativeDicTraverseSession(String locale, long dictSize) { + return setDicTraverseSessionNative(locale, dictSize); } private void closeInternal() { diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 8a3a88438..fa79f5af7 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -137,7 +137,10 @@ public abstract class Dictionary { } /** - * Whether we think this suggestion should trigger an auto-commit. + * Whether we think this suggestion should trigger an auto-commit. prevWord is the word + * before the suggestion, so that we can use n-gram frequencies. + * @param candidate The candidate suggestion, in whole (not only the first part). + * @return whether we should auto-commit or not. */ 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 diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index c884e7b1f..2a9076436 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -617,4 +617,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { }); return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); } + + @UsedForTesting + public void shutdownExecutorForTests() { + getExecutor(mFilename).shutdown(); + } + + @UsedForTesting + public boolean isTerminatedForTests() { + return getExecutor(mFilename).isTerminated(); + } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index ba7d1a2b0..d491f988a 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -344,7 +344,8 @@ public class ExpandableDictionary extends Dictionary { // in the future. suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false; } if (null != node.mShortcutTargets) { @@ -353,7 +354,8 @@ public class ExpandableDictionary extends Dictionary { final char[] shortcut = node.mShortcutTargets.get(shortcutIndex); suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length), finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false; } } @@ -604,7 +606,8 @@ public class ExpandableDictionary extends Dictionary { suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index, Constants.DICTIONARY_MAX_WORD_LENGTH - index), freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); } } } diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index e96a46e12..2e638aaf3 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -105,6 +105,17 @@ public final class InputPointers { mTimes.append(times, startPos, length); } + /** + * Shift to the left by elementCount, discarding elementCount pointers at the start. + * @param elementCount how many elements to shift. + */ + public void shift(final int elementCount) { + mXCoordinates.shift(elementCount); + mYCoordinates.shift(elementCount); + mPointerIds.shift(elementCount); + mTimes.shift(elementCount); + } + public void reset() { final int defaultCapacity = mDefaultCapacity; mXCoordinates.reset(defaultCapacity); diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 6c83ac7ed..d3a18d410 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -95,7 +95,6 @@ import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; -import com.android.inputmethod.latin.utils.PositionalInfoForUserDictPendingAddition; import com.android.inputmethod.latin.utils.RecapitalizeStatus; import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; @@ -185,8 +184,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mIsUserDictionaryAvailable; private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private PositionalInfoForUserDictPendingAddition - mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); private final RichInputConnection mConnection = new RichInputConnection(this); private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); @@ -898,19 +895,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); - // If we have a user dictionary addition in progress, we should check now if we should - // replace the previously committed string with the word that has actually been added - // to the user dictionary. - if (null != mPositionalInfoForUserDictPendingAddition - && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( - mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { - mPositionalInfoForUserDictPendingAddition = null; - } - // If tryReplaceWithActualWord returns false, we don't know what word was - // added to the user dictionary yet, so we keep the data and defer processing. The word will - // 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"); @@ -1250,7 +1234,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen int visibleTopY = extraHeight; // Need to set touchable region only if input view is being shown if (visibleKeyboardView.isShown()) { - if (mSuggestionStripView.getVisibility() == View.VISIBLE) { + // Note that the height of Emoji layout is the same as the height of the main keyboard + // and the suggestion strip + if (mKeyboardSwitcher.isShowingEmojiKeyboard() + || mSuggestionStripView.getVisibility() == View.VISIBLE) { visibleTopY -= suggestionsHeight; } final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; @@ -1416,7 +1403,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void addWordToUserDictionary(final String word) { if (TextUtils.isEmpty(word)) { // Probably never supposed to happen, but just in case. - mPositionalInfoForUserDictPendingAddition = null; return; } final String wordToEdit; @@ -1428,22 +1414,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mUserDictionary.addWordToUserDictionary(wordToEdit); } - public void onWordAddedToUserDictionary(final String newSpelling) { - // If word was added but not by us, bail out - if (null == mPositionalInfoForUserDictPendingAddition) return; - if (mWordComposer.isComposingWord()) { - // We are late... give up and return - mPositionalInfoForUserDictPendingAddition = null; - return; - } - mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling); - if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( - mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd, - mSubtypeSwitcher.getCurrentSubtypeLocale())) { - mPositionalInfoForUserDictPendingAddition = null; - } - } - private void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; showSubtypeSelectorAndSettings(); @@ -1877,10 +1847,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onUpdateBatchInput(final InputPointers batchPointers) { - final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); - if (null != candidate) { - if (candidate.mSourceDict.shouldAutoCommit(candidate)) { - // TODO: implement auto-commit + if (mSettings.getCurrent().mPhraseGestureEnabled) { + final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); + if (null != candidate) { + if (candidate.mSourceDict.shouldAutoCommit(candidate)) { + final String[] commitParts = candidate.mWord.split(" ", 2); + batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); + promotePhantomSpace(); + mConnection.commitText(commitParts[0], 0); + mSpaceState = SPACE_STATE_PHANTOM; + mKeyboardSwitcher.updateShiftState(); + mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); + } } } mInputUpdater.onUpdateBatchInput(batchPointers); @@ -1893,12 +1871,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (TextUtils.isEmpty(batchInputText)) { return; } - mWordComposer.setBatchInputWord(batchInputText); mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState) { promotePhantomSpace(); } - mConnection.setComposingText(batchInputText, 1); + if (mSettings.getCurrent().mPhraseGestureEnabled) { + // Find the last space + final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; + if (0 != indexOfLastSpace) { + mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); + } + final String lastWord = batchInputText.substring(indexOfLastSpace); + mWordComposer.setBatchInputWord(lastWord); + mConnection.setComposingText(lastWord, 1); + } else { + mWordComposer.setBatchInputWord(batchInputText); + mConnection.setComposingText(batchInputText, 1); + } mExpectingUpdateSelection = true; mConnection.endBatchEdit(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { @@ -2704,6 +2694,8 @@ 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; + // A simple way to test for support from the TextView. + if (!isSuggestionsStripVisible()) return; // Recorrection is not supported in languages without spaces because we don't know // how to segment them yet. if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; @@ -2730,7 +2722,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen suggestions.add(new SuggestedWordInfo(s, SuggestionStripView.MAX_SUGGESTIONS - i, SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE + /* autoCommitFirstWordConfidence */)); } } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 18ba15872..7815f4d41 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -48,8 +48,9 @@ public final class Suggest { // Session id for // {@link #getSuggestedWords(WordComposer,String,ProximityInfo,boolean,int)}. + // We are sharing the same ID between typing and gesture to save RAM footprint. public static final int SESSION_TYPING = 0; - public static final int SESSION_GESTURE = 1; + public static final int SESSION_GESTURE = 0; // TODO: rename this to CORRECTION_OFF public static final int CORRECTION_NONE = 0; @@ -326,7 +327,8 @@ public final class Suggest { suggestionsContainer.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); } SuggestedWordInfo.removeDups(suggestionsContainer); @@ -473,7 +475,8 @@ public final class Suggest { sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE); } return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKind, - wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord); + wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); } public void close() { diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index b27fd81e9..fed4cdbbb 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -114,7 +114,8 @@ public final class SuggestedWords { final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(), SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED, Dictionary.DICTIONARY_APPLICATION_DEFINED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); result.add(suggestedWordInfo); } return result; @@ -128,7 +129,8 @@ public final class SuggestedWords { final HashSet<String> alreadySeen = CollectionUtils.newHashSet(); suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); alreadySeen.add(typedWord.toString()); final int previousSize = previousSuggestions.size(); for (int index = 1; index < previousSize; index++) { @@ -151,6 +153,7 @@ public final class SuggestedWords { public static final class SuggestedWordInfo { public static final int NOT_AN_INDEX = -1; + public static final int NOT_A_CONFIDENCE = -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 @@ -180,16 +183,30 @@ public final class SuggestedWords { // passed to native code to get suggestions for a gesture that corresponds to the first // letter of the second word. public final int mIndexOfTouchPointOfSecondWord; + // For auto-commit. This is a measure of how confident we are that we can commit the + // first word of this suggestion. + public final int mAutoCommitFirstWordConfidence; private String mDebugString = ""; + /** + * Create a new suggested word info. + * @param word The string to suggest. + * @param score A measure of how likely this suggestion is. + * @param kind The kind of suggestion, as one of the above KIND_* constants. + * @param sourceDict What instance of Dictionary produced this suggestion. + * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord. + * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence. + */ public SuggestedWordInfo(final String word, final int score, final int kind, - final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord) { + final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, + final int autoCommitFirstWordConfidence) { mWord = word; mScore = score; mKind = kind; mSourceDict = sourceDict; mCodePointCount = StringUtils.codePointCount(mWord); mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord; + mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence; } public boolean isEligibleForAutoCommit() { @@ -259,4 +276,24 @@ public final class SuggestedWords { false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions, mIsPrediction); } + + // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the + // last word of all suggestions, separated by a space. This is necessary because when we commit + // a multiple-word suggestion, the IME only retains the last word as the composing word, and + // we should only suggest replacements for this last word. + // TODO: make this work with languages without spaces. + public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() { + final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { + final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); + final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1; + final String lastWord = info.mWord.substring(indexOfLastSpace); + newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKind, + info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX, + SuggestedWordInfo.NOT_A_CONFIDENCE)); + } + return new SuggestedWords(newSuggestions, mTypedWordValid, + mWillAutoCorrect, mIsPunctuationSuggestions, mIsObsoleteSuggestions, + mIsPrediction); + } } diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index b2bb61596..a241b5505 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -103,14 +103,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { @Override public void onChange(final boolean self, final Uri uri) { setRequiresReload(true); - // We want to report back to Latin IME in case the user just entered the word. - // If the user changed the word in the dialog box, then we want to replace - // what was entered in the text field. - if (null == uri || !(context instanceof LatinIME)) return; - final long changedRowId = ContentUris.parseId(uri); - if (-1 == changedRowId) return; // Unknown content... Not sure why we're here - final String changedWord = getChangedWordForUri(uri); - ((LatinIME)context).onWordAddedToUserDictionary(changedWord); } }; cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); @@ -118,19 +110,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { loadDictionary(); } - private String getChangedWordForUri(final Uri uri) { - final Cursor cursor = mContext.getContentResolver().query(uri, - PROJECTION_QUERY, null, null, null); - if (cursor == null) return null; - try { - if (!cursor.moveToFirst()) return null; - final int indexWord = cursor.getColumnIndex(Words.WORD); - return cursor.getString(indexWord); - } finally { - cursor.close(); - } - } - @Override public synchronized void close() { if (mObserver != null) { diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java index ceb8fa81f..5b319ad90 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java @@ -342,13 +342,11 @@ public final class BinaryDictDecoderUtils { * @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(); + /* package for tests */ static WeightedString getWordAtPosition(final DictDecoder dictDecoder, + final int headerSize, final int pos, final FormatOptions formatOptions) { final WeightedString result; - final int originalPos = dictBuffer.position(); - dictBuffer.position(pos); + final int originalPos = dictDecoder.getPosition(); + dictDecoder.setPosition(pos); if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { result = getWordAtPositionWithParentAddress(dictDecoder, pos, formatOptions); @@ -357,14 +355,13 @@ public final class BinaryDictDecoderUtils { formatOptions); } - dictBuffer.position(originalPos); + dictDecoder.setPosition(originalPos); return result; } @SuppressWarnings("unused") - private static WeightedString getWordAtPositionWithParentAddress( - final Ver3DictDecoder dictDecoder, final int pos, final FormatOptions options) { - final DictBuffer dictBuffer = dictDecoder.getDictBuffer(); + private static WeightedString getWordAtPositionWithParentAddress(final DictDecoder dictDecoder, + final int pos, final FormatOptions options) { int currentPos = pos; int frequency = Integer.MIN_VALUE; final StringBuilder builder = new StringBuilder(); @@ -373,7 +370,7 @@ public final class BinaryDictDecoderUtils { PtNodeInfo currentInfo; int loopCounter = 0; do { - dictBuffer.position(currentPos); + dictDecoder.setPosition(currentPos); currentInfo = dictDecoder.readPtNode(currentPos, options); if (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)) { currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress; @@ -392,11 +389,10 @@ public final class BinaryDictDecoderUtils { } private static WeightedString getWordAtPositionWithoutParentAddress( - final Ver3DictDecoder dictDecoder, final int headerSize, final int pos, + final DictDecoder dictDecoder, final int headerSize, final int pos, final FormatOptions options) { - final DictBuffer dictBuffer = dictDecoder.getDictBuffer(); - dictBuffer.position(headerSize); - final int count = readPtNodeCount(dictBuffer); + dictDecoder.setPosition(headerSize); + final int count = dictDecoder.readPtNodeCount(); int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count); final StringBuilder builder = new StringBuilder(); WeightedString result = null; @@ -414,8 +410,8 @@ public final class BinaryDictDecoderUtils { 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); + dictDecoder.setPosition(last.mChildrenAddress); + i = dictDecoder.readPtNodeCount(); groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); last = null; continue; @@ -424,8 +420,8 @@ public final class BinaryDictDecoderUtils { } if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - dictBuffer.position(last.mChildrenAddress); - i = readPtNodeCount(dictBuffer); + dictDecoder.setPosition(last.mChildrenAddress); + i = dictDecoder.readPtNodeCount(); groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); last = null; continue; @@ -449,17 +445,16 @@ public final class BinaryDictDecoderUtils { * @param options file format options. * @return the read node array with all his children already read. */ - private static PtNodeArray readNodeArray(final Ver3DictDecoder dictDecoder, + private static PtNodeArray readNodeArray(final DictDecoder 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(); + final int nodeArrayOriginPos = dictDecoder.getPosition(); do { // Scan the linked-list node. - final int nodeArrayHeadPos = dictBuffer.position(); - final int count = readPtNodeCount(dictBuffer); + final int nodeArrayHeadPos = dictDecoder.getPosition(); + final int count = dictDecoder.readPtNodeCount(); int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count); for (int i = count; i > 0; --i) { // Scan the array of PtNode. PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options); @@ -480,11 +475,11 @@ public final class BinaryDictDecoderUtils { if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { PtNodeArray children = reverseNodeArrayMap.get(info.mChildrenAddress); if (null == children) { - final int currentPosition = dictBuffer.position(); - dictBuffer.position(info.mChildrenAddress); + final int currentPosition = dictDecoder.getPosition(); + dictDecoder.setPosition(info.mChildrenAddress); children = readNodeArray(dictDecoder, headerSize, reverseNodeArrayMap, reversePtNodeMap, options); - dictBuffer.position(currentPosition); + dictDecoder.setPosition(currentPosition); } nodeArrayContents.add( new PtNode(info.mCharacters, shortcutTargets, bigrams, @@ -503,15 +498,10 @@ public final class BinaryDictDecoderUtils { // 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; - } + final boolean hasValidForwardLink = dictDecoder.readForwardLinkAndAdvancePosition(); + if (!hasValidForwardLink) break; } - } while (options.mSupportsDynamicUpdate && - dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS); + } while (options.mSupportsDynamicUpdate && dictDecoder.hasNextPtNodeArray()); final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents); nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos; diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java index 5a213415a..f333b0d86 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java @@ -126,8 +126,14 @@ public class BinaryDictEncoderUtils { */ 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; + if (ptNode.isTerminal()) { + // If terminal, one byte for the frequency or four bytes for the terminal id. + if (options.mHasTerminalId) { + size += FormatSpec.PTNODE_TERMINAL_ID_SIZE; + } else { + size += FormatSpec.PTNODE_FREQUENCY_SIZE; + } + } size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address size += getShortcutListSize(ptNode.mShortcutTargets); if (null != ptNode.mBigrams) { @@ -198,6 +204,27 @@ public class BinaryDictEncoderUtils { } } + static int writeUIntToBuffer(final byte[] buffer, int position, final int value, + final int size) { + switch(size) { + case 4: + buffer[position++] = (byte) ((value >> 24) & 0xFF); + /* fall through */ + case 3: + buffer[position++] = (byte) ((value >> 16) & 0xFF); + /* fall through */ + case 2: + buffer[position++] = (byte) ((value >> 8) & 0xFF); + /* fall through */ + case 1: + buffer[position++] = (byte) (value & 0xFF); + break; + default: + /* nop */ + } + return position; + } + // End utility methods // This method is responsible for finding a nice ordering of the nodes that favors run-time @@ -324,7 +351,13 @@ public class BinaryDictEncoderUtils { changed = true; } int nodeSize = getNodeHeaderSize(ptNode, formatOptions); - if (ptNode.isTerminal()) nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE; + if (ptNode.isTerminal()) { + if (formatOptions.mHasTerminalId) { + nodeSize += FormatSpec.PTNODE_TERMINAL_ID_SIZE; + } else { + nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE; + } + } if (formatOptions.mSupportsDynamicUpdate) { nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; } else if (null != ptNode.mChildren) { @@ -733,7 +766,7 @@ public class BinaryDictEncoderUtils { } /** - * Write a PtNodeArray to memory. The PtNodeArray is expected to have its final position cached. + * Write a PtNodeArray. 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 dictEncoder the dictionary encoder. @@ -741,7 +774,7 @@ public class BinaryDictEncoderUtils { * @param formatOptions file format options. */ @SuppressWarnings("unused") - /* package */ static void writePlacedNode(final FusionDictionary dict, + /* package */ static void writePlacedPtNodeArray(final FusionDictionary dict, final DictEncoder dictEncoder, final PtNodeArray ptNodeArray, final FormatOptions formatOptions) { // TODO: Make the code in common with BinaryDictIOUtils#writePtNode @@ -766,14 +799,7 @@ public class BinaryDictEncoderUtils { + FormatSpec.MAX_TERMINAL_FREQUENCY + " : " + ptNode.mFrequency); } - - dictEncoder.writePtNodeFlags(ptNode, parentPosition, formatOptions); - dictEncoder.writeParentPosition(parentPosition, ptNode, formatOptions); - dictEncoder.writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); - dictEncoder.writeFrequency(ptNode.mFrequency); - dictEncoder.writeChildrenPosition(ptNode, formatOptions); - dictEncoder.writeShortcuts(ptNode.mShortcutTargets); - dictEncoder.writeBigrams(ptNode.mBigrams, dict); + dictEncoder.writePtNode(ptNode, parentPosition, formatOptions, dict); } if (formatOptions.mSupportsDynamicUpdate) { dictEncoder.writeForwardLinkAddress(FormatSpec.NO_FORWARD_LINK_ADDRESS); diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java index 106f02519..2c5e93e5c 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -61,12 +61,11 @@ public final class BinaryDictIOUtils { /** * Retrieves all node arrays without recursive call. */ - private static void readUnigramsAndBigramsBinaryInner( - final Ver3DictDecoder dictDecoder, final int headerSize, - final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + private static void readUnigramsAndBigramsBinaryInner(final DictDecoder 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>(); @@ -83,11 +82,11 @@ public final class BinaryDictIOUtils { p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength); } - if (dictBuffer.position() != p.mAddress) dictBuffer.position(p.mAddress); + if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress); if (index != p.mLength) index = p.mLength; if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) { - p.mNumOfPtNode = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); + p.mNumOfPtNode = dictDecoder.readPtNodeCount(); p.mAddress += getPtNodeCountSize(p.mNumOfPtNode); p.mPosition = 0; } @@ -114,11 +113,12 @@ public final class BinaryDictIOUtils { if (p.mPosition == p.mNumOfPtNode) { if (formatOptions.mSupportsDynamicUpdate) { - final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); - if (forwardLinkAddress != FormatSpec.NO_FORWARD_LINK_ADDRESS) { + final boolean hasValidForwardLinkAddress = + dictDecoder.readForwardLinkAndAdvancePosition(); + if (hasValidForwardLinkAddress && dictDecoder.hasNextPtNodeArray()) { // The node array has a forward link. p.mNumOfPtNode = Position.NOT_READ_PTNODE_COUNT; - p.mAddress = forwardLinkAddress; + p.mAddress = dictDecoder.getPosition(); } else { stack.pop(); } @@ -127,7 +127,7 @@ public final class BinaryDictIOUtils { } } else { // The Ptnode array has more PtNodes. - p.mAddress = dictBuffer.position(); + p.mAddress = dictDecoder.getPosition(); } if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) { @@ -148,7 +148,7 @@ public final class BinaryDictIOUtils { * @throws IOException if the file can't be read. * @throws UnsupportedFormatException if the format of the file is not recognized. */ - /* package */ static void readUnigramsAndBigramsBinary(final Ver3DictDecoder dictDecoder, + /* package */ static void readUnigramsAndBigramsBinary(final DictDecoder dictDecoder, final Map<Integer, String> words, final Map<Integer, Integer> frequencies, final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, UnsupportedFormatException { @@ -169,11 +169,10 @@ public final class BinaryDictIOUtils { * @throws UnsupportedFormatException if the format of the file is not recognized. */ @UsedForTesting - /* package */ static int getTerminalPosition(final Ver3DictDecoder dictDecoder, + /* package */ static int getTerminalPosition(final DictDecoder dictDecoder, final String word) throws IOException, UnsupportedFormatException { - final DictBuffer dictBuffer = dictDecoder.getDictBuffer(); if (word == null) return FormatSpec.NOT_VALID_WORD; - if (dictBuffer.position() != 0) dictBuffer.position(0); + dictDecoder.setPosition(0); final FileHeader header = dictDecoder.readHeader(); int wordPos = 0; @@ -182,10 +181,10 @@ public final class BinaryDictIOUtils { if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD; do { - final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); + final int ptNodeCount = dictDecoder.readPtNodeCount(); boolean foundNextPtNode = false; for (int i = 0; i < ptNodeCount; ++i) { - final int ptNodePos = dictBuffer.position(); + final int ptNodePos = dictDecoder.getPosition(); final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos, header.mFormatOptions); final boolean isMovedNode = isMovedPtNode(currentInfo.mFlags, @@ -219,7 +218,7 @@ public final class BinaryDictIOUtils { return FormatSpec.NOT_VALID_WORD; } foundNextPtNode = true; - dictBuffer.position(currentInfo.mChildrenAddress); + dictDecoder.setPosition(currentInfo.mChildrenAddress); break; } } @@ -233,11 +232,11 @@ public final class BinaryDictIOUtils { return FormatSpec.NOT_VALID_WORD; } - final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); - if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) { + final boolean hasValidForwardLinkAddress = + dictDecoder.readForwardLinkAndAdvancePosition(); + if (!hasValidForwardLinkAddress || !dictDecoder.hasNextPtNodeArray()) { return FormatSpec.NOT_VALID_WORD; } - dictBuffer.position(forwardLinkAddress); } while(true); } return FormatSpec.NOT_VALID_WORD; @@ -520,7 +519,7 @@ public final class BinaryDictIOUtils { final File file, final long offset, final long length) throws FileNotFoundException, IOException, UnsupportedFormatException { final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE]; - final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file, + final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, new DictDecoder.DictionaryBufferFactory() { @Override public DictBuffer getDictionaryBuffer(File file) diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java index 11a3f0b3a..40e852423 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java @@ -54,10 +54,13 @@ public interface DictDecoder { * 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. + * @param deleteDictIfBroken a flag indicating whether this method should remove the broken + * dictionary or not. * @return the created (or merged) dictionary. */ @UsedForTesting - public FusionDictionary readDictionaryBinary(final FusionDictionary dict) + public FusionDictionary readDictionaryBinary(final FusionDictionary dict, + final boolean deleteDictIfBroken) throws FileNotFoundException, IOException, UnsupportedFormatException; /** @@ -88,6 +91,41 @@ public interface DictDecoder { final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, UnsupportedFormatException; + /** + * Sets the position of the buffer to the given value. + * + * @param newPos the new position + */ + public void setPosition(final int newPos); + + /** + * Gets the position of the buffer. + * + * @return the position + */ + public int getPosition(); + + /** + * Reads and returns the PtNode count out of a buffer and forwards the pointer. + */ + public int readPtNodeCount(); + + /** + * Reads the forward link and advances the position. + * + * @return if this method advances the position then true else false. + */ + public boolean readForwardLinkAndAdvancePosition(); + public boolean hasNextPtNodeArray(); + + /** + * Opens the dictionary file and makes DictBuffer. + */ + @UsedForTesting + public void openDictBuffer() throws FileNotFoundException, IOException; + @UsedForTesting + public boolean isOpenedDictBuffer(); + // Flags for DictionaryBufferFactory. public static final int USE_READONLY_BYTEBUFFER = 0x01000000; public static final int USE_BYTEARRAY = 0x02000000; diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java index d1589a30e..ea5d492d8 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java @@ -18,10 +18,8 @@ package com.android.inputmethod.latin.makedict; 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 java.io.IOException; -import java.util.ArrayList; /** * An interface of binary dictionary encoder. @@ -33,28 +31,8 @@ public interface DictEncoder { public void setPosition(final int position); public int getPosition(); public void writePtNodeCount(final int ptNodeCount); - public void writePtNodeFlags(final PtNode ptNode, final int parentAddress, - final FormatOptions formatOptions); - public void writeParentPosition(final int parentPosition, final PtNode ptNode, - final FormatOptions formatOptions); - public void writeCharacters(final int[] characters, final boolean hasSeveralChars); - public void writeFrequency(final int frequency); - public void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions); - - /** - * Write a shortcut attributes list to memory. - * - * @param shortcuts the shortcut attributes list. - */ - public void writeShortcuts(final ArrayList<WeightedString> shortcuts); - - /** - * Write a bigram attributes list to memory. - * - * @param bigrams the bigram attributes list. - * @param dict the dictionary the node array is a part of (for relative offsets). - */ - public void writeBigrams(final ArrayList<WeightedString> bigrams, final FusionDictionary dict); - public void writeForwardLinkAddress(final int forwardLinkAddress); + + public void writePtNode(final PtNode ptNode, final int parentPosition, + final FormatOptions formatOptions, final FusionDictionary dict); } diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index bf35f6a8a..96ccd8e49 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -18,8 +18,11 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory; import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; +import java.io.File; + /** * Dictionary File Format Specification. */ @@ -195,9 +198,12 @@ public final class FormatSpec { public static final int MAGIC_NUMBER = 0x9BC13AFE; static final int MINIMUM_SUPPORTED_VERSION = 2; - static final int MAXIMUM_SUPPORTED_VERSION = 3; + static final int MAXIMUM_SUPPORTED_VERSION = 4; static final int NOT_A_VERSION_NUMBER = -1; static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3; + static final int FIRST_VERSION_WITH_TERMINAL_ID = 4; + static final int VERSION3 = 3; + static final int VERSION4 = 4; // These options need to be the same numeric values as the one in the native reading code. static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; @@ -248,11 +254,17 @@ public final class FormatSpec { 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_TERMINAL_ID_SIZE = 4; 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; + // These values are used only by version 4 or later. + static final String TRIE_FILE_EXTENSION = ".trie"; + static final String FREQ_FILE_EXTENSION = ".freq"; + static final int FREQUENCY_AND_FLAGS_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; @@ -261,6 +273,7 @@ public final class FormatSpec { 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_SHORTCUT_LIST_SIZE_IN_A_PTNODE = 0xFFFF; static final int MAX_TERMINAL_FREQUENCY = 255; static final int MAX_BIGRAM_FREQUENCY = 15; @@ -284,6 +297,7 @@ public final class FormatSpec { public static final class FormatOptions { public final int mVersion; public final boolean mSupportsDynamicUpdate; + public final boolean mHasTerminalId; @UsedForTesting public FormatOptions(final int version) { this(version, false); @@ -297,6 +311,7 @@ public final class FormatSpec { + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior."); } mSupportsDynamicUpdate = supportsDynamicUpdate; + mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID); } } @@ -341,6 +356,28 @@ public final class FormatSpec { } } + /** + * Returns new dictionary decoder. + * + * @param dictFile the dictionary file. + * @param bufferType the flag indicating buffer type which is used by the dictionary decoder. + * @return new dictionary decoder if the dictionary file exists, otherwise null. + */ + public static DictDecoder getDictDecoder(final File dictFile, final int bufferType) { + if (!dictFile.isFile()) return null; + return new Ver3DictDecoder(dictFile, bufferType); + } + + public static DictDecoder getDictDecoder(final File dictFile, + final DictionaryBufferFactory factory) { + if (!dictFile.isFile()) return null; + return new Ver3DictDecoder(dictFile, factory); + } + + public static DictDecoder getDictDecoder(final File dictFile) { + return getDictDecoder(dictFile, DictDecoder.USE_READONLY_BYTEBUFFER); + } + private FormatSpec() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 3e685a3d7..be653feec 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -111,6 +111,7 @@ public final class FusionDictionary implements Iterable<Word> { ArrayList<WeightedString> mShortcutTargets; ArrayList<WeightedString> mBigrams; int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. + int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal. PtNodeArray mChildren; boolean mIsNotAWord; // Only a shortcut boolean mIsBlacklistEntry; @@ -129,6 +130,7 @@ public final class FusionDictionary implements Iterable<Word> { final boolean isNotAWord, final boolean isBlacklistEntry) { mChars = chars; mFrequency = frequency; + mTerminalId = frequency; mShortcutTargets = shortcutTargets; mBigrams = bigrams; mChildren = null; @@ -156,6 +158,10 @@ public final class FusionDictionary implements Iterable<Word> { mChildren.mData.add(n); } + public int getTerminalId() { + return mTerminalId; + } + public boolean isTerminal() { return NOT_A_TERMINAL != mFrequency; } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java index 1a5023ef6..1a90a4b98 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java @@ -173,11 +173,7 @@ public class Ver3DictDecoder implements DictDecoder { 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) { + /* package */ Ver3DictDecoder(final File file, final int factoryFlag) { mDictionaryBinaryFile = file; mDictBuffer = null; @@ -192,15 +188,21 @@ public class Ver3DictDecoder implements DictDecoder { } } - public Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) { + /* package */ Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) { mDictionaryBinaryFile = file; mBufferFactory = factory; } + @Override public void openDictBuffer() throws FileNotFoundException, IOException { mDictBuffer = mBufferFactory.getDictionaryBuffer(mDictionaryBinaryFile); } + @Override + public boolean isOpenedDictBuffer() { + return mDictBuffer != null; + } + /* package */ DictBuffer getDictBuffer() { return mDictBuffer; } @@ -306,7 +308,8 @@ public class Ver3DictDecoder implements DictDecoder { } @Override - public FusionDictionary readDictionaryBinary(final FusionDictionary dict) + public FusionDictionary readDictionaryBinary(final FusionDictionary dict, + final boolean deleteDictIfBroken) throws FileNotFoundException, IOException, UnsupportedFormatException { if (mDictBuffer == null) { openDictBuffer(); @@ -315,13 +318,13 @@ public class Ver3DictDecoder implements DictDecoder { return BinaryDictDecoderUtils.readDictionaryBinary(this, dict); } catch (IOException e) { Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e); - if (!mDictionaryBinaryFile.delete()) { + if (deleteDictIfBroken && !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()) { + if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) { Log.e(TAG, "Failed to delete the broken dictionary."); } throw e; @@ -347,4 +350,33 @@ public class Ver3DictDecoder implements DictDecoder { BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams); } + @Override + public void setPosition(int newPos) { + mDictBuffer.position(newPos); + } + + @Override + public int getPosition() { + return mDictBuffer.position(); + } + + @Override + public int readPtNodeCount() { + return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer); + } + + @Override + public boolean readForwardLinkAndAdvancePosition() { + final int nextAddress = mDictBuffer.readUnsignedInt24(); + if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) { + mDictBuffer.position(nextAddress); + return true; + } + return false; + } + + @Override + public boolean hasNextPtNodeArray() { + return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java index 3f26ff378..222a0f474 100644 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java +++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java @@ -68,7 +68,7 @@ public class Ver3DictEncoder implements DictEncoder { @Override public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) throws IOException, UnsupportedFormatException { - if (formatOptions.mVersion > 3) { + if (formatOptions.mVersion > FormatSpec.VERSION3) { throw new UnsupportedFormatException( "The given format options has wrong version number : " + formatOptions.mVersion); @@ -103,7 +103,7 @@ public class Ver3DictEncoder implements DictEncoder { MakedictLog.i("Writing file..."); for (PtNodeArray nodeArray : flatNodes) { - BinaryDictEncoderUtils.writePlacedNode(dict, this, nodeArray, formatOptions); + BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); } if (MakedictLog.DBG) BinaryDictEncoderUtils.showStatistics(flatNodes); mOutStream.write(mBuffer, 0, mPosition); @@ -126,26 +126,23 @@ public class Ver3DictEncoder implements DictEncoder { @Override public void writePtNodeCount(final int ptNodeCount) { final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); - if (1 == countSize) { - mBuffer[mPosition++] = (byte) ptNodeCount; - } else if (2 == countSize) { - mBuffer[mPosition++] = (byte) ((ptNodeCount >> 8) & 0xFF); - mBuffer[mPosition++] = (byte) (ptNodeCount & 0xFF); - } else { + if (countSize != 1 && countSize != 2) { throw new RuntimeException("Strange size from getGroupCountSize : " + countSize); } + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, ptNodeCount, + countSize); } - @Override - public void writePtNodeFlags(final PtNode ptNode, final int parentAddress, + private void writePtNodeFlags(final PtNode ptNode, final int parentAddress, final FormatOptions formatOptions) { final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - mBuffer[mPosition++] = BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, - childrenPos, formatOptions); + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, + BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, childrenPos, + formatOptions), + FormatSpec.PTNODE_FLAGS_SIZE); } - @Override - public void writeParentPosition(final int parentPosition, final PtNode ptNode, + private void writeParentPosition(final int parentPosition, final PtNode ptNode, final FormatOptions formatOptions) { if (parentPosition == FormatSpec.NO_PARENT_ADDRESS) { mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition, @@ -156,22 +153,20 @@ public class Ver3DictEncoder implements DictEncoder { } } - @Override - public void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) { + private void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) { mPosition = CharEncoding.writeCharArray(codePoints, mBuffer, mPosition); if (hasSeveralChars) { mBuffer[mPosition++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; } } - @Override - public void writeFrequency(final int frequency) { + private void writeFrequency(final int frequency) { if (frequency >= 0) { - mBuffer[mPosition++] = (byte) frequency; + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, frequency, + FormatSpec.PTNODE_FREQUENCY_SIZE); } } - @Override public void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions) { final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); if (formatOptions.mSupportsDynamicUpdate) { @@ -183,8 +178,12 @@ public class Ver3DictEncoder implements DictEncoder { } } - @Override - public void writeShortcuts(final ArrayList<WeightedString> shortcuts) { + /** + * Write a shortcut attributes list to mBuffer. + * + * @param shortcuts the shortcut attributes list. + */ + private void writeShortcuts(final ArrayList<WeightedString> shortcuts) { if (null == shortcuts || shortcuts.isEmpty()) return; final int indexOfShortcutByteSize = mPosition; @@ -195,20 +194,27 @@ public class Ver3DictEncoder implements DictEncoder { final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( shortcutIterator.hasNext(), target.mFrequency); - mBuffer[mPosition++] = (byte)shortcutFlags; + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, shortcutFlags, + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); final int shortcutShift = CharEncoding.writeString(mBuffer, mPosition, target.mWord); mPosition += shortcutShift; } final int shortcutByteSize = mPosition - indexOfShortcutByteSize; - if (shortcutByteSize > 0xFFFF) { + if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) { throw new RuntimeException("Shortcut list too large"); } - mBuffer[indexOfShortcutByteSize] = (byte)((shortcutByteSize >> 8) & 0xFF); - mBuffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF); - } - - @Override - public void writeBigrams(final ArrayList<WeightedString> bigrams, final FusionDictionary dict) { + BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, indexOfShortcutByteSize, shortcutByteSize, + FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); + } + + /** + * Write a bigram attributes list to mBuffer. + * + * @param bigrams the bigram attributes list. + * @param dict the dictionary the node array is a part of (for relative offsets). + */ + private void writeBigrams(final ArrayList<WeightedString> bigrams, + final FusionDictionary dict) { if (bigrams == null) return; final Iterator<WeightedString> bigramIterator = bigrams.iterator(); @@ -220,9 +226,10 @@ public class Ver3DictEncoder implements DictEncoder { final int unigramFrequencyForThisWord = target.mFrequency; final int offset = addressOfBigram - (mPosition + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), + final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord); - mBuffer[mPosition++] = (byte) bigramFlags; + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, bigramFlags, + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, Math.abs(offset)); } @@ -230,8 +237,19 @@ public class Ver3DictEncoder implements DictEncoder { @Override public void writeForwardLinkAddress(final int forwardLinkAddress) { - mBuffer[mPosition++] = (byte) ((forwardLinkAddress >> 16) & 0xFF); - mBuffer[mPosition++] = (byte) ((forwardLinkAddress >> 8) & 0xFF); - mBuffer[mPosition++] = (byte) (forwardLinkAddress & 0xFF); + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, forwardLinkAddress, + FormatSpec.FORWARD_LINK_ADDRESS_SIZE); + } + + @Override + public void writePtNode(final PtNode ptNode, final int parentPosition, + final FormatOptions formatOptions, final FusionDictionary dict) { + writePtNodeFlags(ptNode, parentPosition, formatOptions); + writeParentPosition(parentPosition, ptNode, formatOptions); + writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); + writeFrequency(ptNode.mFrequency); + writeChildrenPosition(ptNode, formatOptions); + writeShortcuts(ptNode.mShortcutTargets); + writeBigrams(ptNode.mBigrams, dict); } } diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java new file mode 100644 index 000000000..75b75ae2e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java @@ -0,0 +1,269 @@ +/* +/* + * 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.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; +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.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * An implementation of DictEncoder for version 4 binary dictionary. + */ +@UsedForTesting +public class Ver4DictEncoder implements DictEncoder { + private final File mDictPlacedDir; + private byte[] mTrieBuf; + private byte[] mFreqBuf; + private int mTriePos; + private OutputStream mTrieOutStream; + private OutputStream mFreqOutStream; + + @UsedForTesting + public Ver4DictEncoder(final File dictPlacedDir) { + mDictPlacedDir = dictPlacedDir; + } + + private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions) + throws FileNotFoundException, IOException { + final FileHeader header = new FileHeader(0, dictOptions, formatOptions); + final String filename = header.getId() + "." + header.getVersion(); + final File mDictDir = new File(mDictPlacedDir, filename); + final File trieFile = new File(mDictDir, filename + FormatSpec.TRIE_FILE_EXTENSION); + final File freqFile = new File(mDictDir, filename + FormatSpec.FREQ_FILE_EXTENSION); + if (!mDictDir.isDirectory()) { + if (mDictDir.exists()) mDictDir.delete(); + mDictDir.mkdirs(); + } + if (!trieFile.exists()) trieFile.createNewFile(); + if (!freqFile.exists()) freqFile.createNewFile(); + mTrieOutStream = new FileOutputStream(trieFile); + mFreqOutStream = new FileOutputStream(freqFile); + } + + private void close() throws IOException { + try { + if (mTrieOutStream != null) { + mTrieOutStream.close(); + } + if (mFreqOutStream != null) { + mFreqOutStream.close(); + } + } finally { + mTrieOutStream = null; + mFreqOutStream = null; + } + } + + @Override + public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) + throws IOException, UnsupportedFormatException { + if (formatOptions.mVersion != FormatSpec.VERSION4) { + throw new UnsupportedFormatException("File header has a wrong version number : " + + formatOptions.mVersion); + } + if (!mDictPlacedDir.isDirectory()) { + throw new UnsupportedFormatException("Given path is not a directory."); + } + + if (mTrieOutStream == null) { + openStreams(formatOptions, dict.mOptions); + } + + BinaryDictEncoderUtils.writeDictionaryHeader(mTrieOutStream, dict, formatOptions); + + MakedictLog.i("Flattening the tree..."); + ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); + int terminalCount = 0; + for (final PtNodeArray array : flatNodes) { + for (final PtNode node : array.mData) { + if (node.isTerminal()) node.mTerminalId = terminalCount++; + } + } + + MakedictLog.i("Computing addresses..."); + BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions); + if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); + + final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); + final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; + mTrieBuf = new byte[bufferSize]; + mFreqBuf = new byte[terminalCount * FormatSpec.FREQUENCY_AND_FLAGS_SIZE]; + + MakedictLog.i("Writing file..."); + for (PtNodeArray nodeArray : flatNodes) { + BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); + } + if (MakedictLog.DBG) { + BinaryDictEncoderUtils.showStatistics(flatNodes); + MakedictLog.i("has " + terminalCount + " terminals."); + } + mTrieOutStream.write(mTrieBuf); + mFreqOutStream.write(mFreqBuf); + + MakedictLog.i("Done"); + close(); + } + + @Override + public void setPosition(int position) { + if (mTrieBuf == null || position < 0 || position >- mTrieBuf.length) return; + mTriePos = position; + } + + @Override + public int getPosition() { + return mTriePos; + } + + @Override + public void writePtNodeCount(int ptNodeCount) { + final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); + // ptNodeCount must fit on one byte or two bytes. + // Please see comments in FormatSpec + if (countSize != 1 && countSize != 2) { + throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize); + } + mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, ptNodeCount, + countSize); + } + + private void writePtNodeFlags(final PtNode ptNode, final int parentAddress, + final FormatOptions formatOptions) { + final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); + mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, + BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mTriePos, childrenPos, + formatOptions), + FormatSpec.PTNODE_FLAGS_SIZE); + } + + private void writeParentPosition(int parentPos, final PtNode ptNode, + final FormatOptions formatOptions) { + if (parentPos != FormatSpec.NO_PARENT_ADDRESS) { + parentPos -= ptNode.mCachedAddressAfterUpdate; + } + mTriePos = BinaryDictEncoderUtils.writeParentAddress(mTrieBuf, mTriePos, parentPos, + formatOptions); + } + + private void writeCharacters(final int[] characters, final boolean hasSeveralChars) { + mTriePos = CharEncoding.writeCharArray(characters, mTrieBuf, mTriePos); + if (hasSeveralChars) { + mTrieBuf[mTriePos++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; + } + } + + private void writeTerminalId(final int terminalId) { + mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, terminalId, + FormatSpec.PTNODE_TERMINAL_ID_SIZE); + } + + private void writeFrequency(final int frequency, final int terminalId) { + final int freqPos = terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE; + BinaryDictEncoderUtils.writeUIntToBuffer(mFreqBuf, freqPos, frequency, + FormatSpec.FREQUENCY_AND_FLAGS_SIZE); + } + + private void writeChildrenPosition(PtNode ptNode, FormatOptions formatOptions) { + final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); + if (formatOptions.mSupportsDynamicUpdate) { + mTriePos += BinaryDictEncoderUtils.writeSignedChildrenPosition(mTrieBuf, + mTriePos, childrenPos); + } else { + mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf, + mTriePos, childrenPos); + } + } + + private void writeShortcuts(ArrayList<WeightedString> shortcuts) { + if (null == shortcuts || shortcuts.isEmpty()) return; + + final int indexOfShortcutByteSize = mTriePos; + mTriePos += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE; + final Iterator<WeightedString> shortcutIterator = shortcuts.iterator(); + while (shortcutIterator.hasNext()) { + final WeightedString target = shortcutIterator.next(); + final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( + shortcutIterator.hasNext(), + target.mFrequency); + mTrieBuf[mTriePos++] = (byte)shortcutFlags; + final int shortcutShift = CharEncoding.writeString(mTrieBuf, mTriePos, + target.mWord); + mTriePos += shortcutShift; + } + final int shortcutByteSize = mTriePos - indexOfShortcutByteSize; + if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) { + throw new RuntimeException("Shortcut list too large : " + shortcutByteSize); + } + BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, indexOfShortcutByteSize, + shortcutByteSize, FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); + } + + private void writeBigrams(ArrayList<WeightedString> bigrams, FusionDictionary dict) { + if (bigrams == null) return; + + final Iterator<WeightedString> bigramIterator = bigrams.iterator(); + while (bigramIterator.hasNext()) { + final WeightedString bigram = bigramIterator.next(); + final PtNode target = + FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); + final int addressOfBigram = target.mCachedAddressAfterUpdate; + final int unigramFrequencyForThisWord = target.mFrequency; + final int offset = addressOfBigram + - (mTriePos + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); + int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), + offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord); + mTrieBuf[mTriePos++] = (byte) bigramFlags; + mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf, + mTriePos, Math.abs(offset)); + } + } + + @Override + public void writeForwardLinkAddress(int forwardLinkAddress) { + mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, + forwardLinkAddress, FormatSpec.FORWARD_LINK_ADDRESS_SIZE); + } + + @Override + public void writePtNode(final PtNode ptNode, final int parentPosition, + final FormatOptions formatOptions, final FusionDictionary dict) { + writePtNodeFlags(ptNode, parentPosition, formatOptions); + writeParentPosition(parentPosition, ptNode, formatOptions); + writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); + if (ptNode.isTerminal()) { + writeTerminalId(ptNode.mTerminalId); + writeFrequency(ptNode.mFrequency, ptNode.mTerminalId); + } + writeChildrenPosition(ptNode, formatOptions); + writeShortcuts(ptNode.mShortcutTargets); + writeBigrams(ptNode.mBigrams, dict); + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java index 5b1d0647b..9364fb034 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java @@ -25,14 +25,13 @@ import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.ExpandableBinaryDictionary; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.Ver3DictDecoder; +import com.android.inputmethod.latin.makedict.FormatSpec; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; @@ -173,16 +172,19 @@ public abstract class DynamicPredictionDictionaryBase extends ExpandableBinaryDi // Load the dictionary from binary file final File dictFile = new File(mContext.getFilesDir(), mFileName); - final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(dictFile, + final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile, DictDecoder.USE_BYTEARRAY); + if (dictDecoder == null) { + // This is an expected condition: we don't have a user history dictionary for this + // language yet. It will be created sometime later. + return; + } + try { 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); + Log.d(TAG, "IOException on opening a bytebuffer", e); } finally { if (PROFILE_SAVE_RESTORE) { final long diff = System.currentTimeMillis() - now; diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 8732a59f8..1a0fecc62 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -81,6 +81,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = "pref_gesture_floating_preview_text"; public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon"; + public static final String PREF_PHRASE_GESTURE_ENABLED = "pref_gesture_space_aware"; public static final String PREF_INPUT_LANGUAGE = "input_language"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; @@ -97,6 +98,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_SEND_FEEDBACK = "send_feedback"; public static final String PREF_ABOUT_KEYBOARD = "about_keyboard"; + // Emoji + public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys"; + public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id"; + private Resources mRes; private SharedPreferences mPrefs; private SettingsValues mSettingsValues; @@ -216,6 +221,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true); } + public static boolean readPhraseGestureEnabled(final SharedPreferences prefs, + final Resources res) { + return prefs.getBoolean(Settings.PREF_PHRASE_GESTURE_ENABLED, + res.getBoolean(R.bool.config_default_phrase_gesture_enabled)); + } + public static boolean readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption( final Resources res) { return res.getBoolean(R.bool.config_enable_show_option_of_key_preview_popup); @@ -363,4 +374,24 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null); return StringUtils.hexStringToByteArray(tokenStr); } + + public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) { + prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply(); + } + + public static String readEmojiRecentKeys(final SharedPreferences prefs) { + return prefs.getString(PREF_EMOJI_RECENT_KEYS, ""); + } + + public static void writeEmojiCategoryLastTypedId( + final SharedPreferences prefs, final int category, final int id) { + final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category; + prefs.edit().putInt(key, id).apply(); + } + + public static int readEmojiCategoryLastTypedId( + final SharedPreferences prefs, final int category) { + final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category; + return prefs.getInt(key, 0); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 072bb8731..ee322e91b 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -76,6 +76,7 @@ public final class SettingsValues { public final boolean mGestureTrailEnabled; public final boolean mGestureFloatingPreviewTextEnabled; public final boolean mSlidingKeyInputPreviewEnabled; + public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; public final Locale mLocale; @@ -159,6 +160,7 @@ public final class SettingsValues { mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); mGestureFloatingPreviewTextEnabled = prefs.getBoolean( Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); + mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res); mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; final String showSuggestionsSetting = prefs.getString( Settings.PREF_SHOW_SUGGESTIONS_SETTING, @@ -211,6 +213,7 @@ public final class SettingsValues { mGestureInputEnabled = true; mGestureTrailEnabled = true; mGestureFloatingPreviewTextEnabled = true; + mPhraseGestureEnabled = true; mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionVisibility = 0; mIsInternal = false; @@ -295,7 +298,8 @@ public final class SettingsValues { puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED, Dictionary.DICTIONARY_HARDCODED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */)); + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); } } return new SuggestedWords(puncList, diff --git a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java deleted file mode 100644 index 1fc7eccc6..000000000 --- a/java/src/com/android/inputmethod/latin/utils/PositionalInfoForUserDictPendingAddition.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2012 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.utils; - -import android.view.inputmethod.EditorInfo; - -import com.android.inputmethod.latin.RichInputConnection; - -import java.util.Locale; - -/** - * Holder class for data about a word already committed but that may still be edited. - * - * When the user chooses to add a word to the user dictionary by pressing the appropriate - * suggestion, a dialog is presented to give a chance to edit the word before it is actually - * registered as a user dictionary word. If the word is actually modified, the IME needs to - * go back and replace the word that was committed with the amended version. - * The word we need to replace with will only be known after it's actually committed, so - * the IME needs to take a note of what it has to replace and where it is. - * This class encapsulates this data. - */ -public final class PositionalInfoForUserDictPendingAddition { - final private String mOriginalWord; - final private int mCursorPos; // Position of the cursor after the word - final private EditorInfo mEditorInfo; // On what binding this has been added - final private int mCapitalizedMode; - private String mActualWordBeingAdded; - - public PositionalInfoForUserDictPendingAddition(final String word, final int cursorPos, - final EditorInfo editorInfo, final int capitalizedMode) { - mOriginalWord = word; - mCursorPos = cursorPos; - mEditorInfo = editorInfo; - mCapitalizedMode = capitalizedMode; - } - - public void setActualWordBeingAdded(final String actualWordBeingAdded) { - mActualWordBeingAdded = actualWordBeingAdded; - } - - /** - * Try to replace the string at the remembered position with the actual word being added. - * - * After the user validated the word being added, the IME has to replace the old version - * (which has been committed in the text view) with the amended version if it's different. - * This method tries to do that, but may fail because the IME is not yet ready to do so - - * for example, it is still waiting for the new string, or it is waiting to return to the text - * view in which the amendment should be made. In these cases, we should keep the data - * and wait until all conditions are met. - * This method returns true if the replacement has been successfully made and this data - * can be forgotten; it returns false if the replacement can't be made yet and we need to - * keep this until a later time. - * The IME knows about the actual word being added through a callback called by the - * user dictionary facility of the device. When this callback comes, the keyboard may still - * be connected to the edition dialog, or it may have already returned to the original text - * field. Replacement has to work in both cases. - * Accordingly, this method is called at two different points in time : upon getting the - * event that a new word was added to the user dictionary, and upon starting up in a - * new text field. - * @param connection The RichInputConnection through which to contact the editor. - * @param editorInfo Information pertaining to the editor we are currently in. - * @param currentCursorPosition The current cursor position, for checking purposes. - * @param locale The locale for changing case, if necessary - * @return true if the edit has been successfully made, false if we need to try again later - */ - public boolean tryReplaceWithActualWord(final RichInputConnection connection, - final EditorInfo editorInfo, final int currentCursorPosition, final Locale locale) { - // If we still don't know the actual word being added, we need to try again later. - if (null == mActualWordBeingAdded) return false; - // The entered text and the registered text were the same anyway : we can - // return success right away even if focus has not returned yet to the text field we - // want to amend. - if (mActualWordBeingAdded.equals(mOriginalWord)) return true; - // Not the same text field : we need to try again later. This happens when the addition - // is reported by the user dictionary provider before the focus has moved back to the - // original text view, so the IME is still in the text view of the dialog and has no way to - // edit the original text view at this time. - if (!mEditorInfo.packageName.equals(editorInfo.packageName) - || mEditorInfo.fieldId != editorInfo.fieldId) { - return false; - } - // Same text field, but not the same cursor position : we give up, so we return success - // so that it won't be tried again - if (currentCursorPosition != mCursorPos) return true; - // We have made all the checks : do the replacement and report success - // If this was auto-capitalized, we need to restore the case before committing - final String wordWithCaseFixed = CapsModeUtils.applyAutoCapsMode(mActualWordBeingAdded, - mCapitalizedMode, locale); - connection.setComposingRegion(currentCursorPosition - mOriginalWord.length(), - currentCursorPosition); - connection.commitText(wordWithCaseFixed, wordWithCaseFixed.length()); - return true; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java index 3c1db6529..5dc0b5893 100644 --- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java +++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java @@ -31,6 +31,7 @@ public class PrioritizedSerialExecutor { private static final int TASK_QUEUE_CAPACITY = 1000; private final Queue<Runnable> mTasks; private final Queue<Runnable> mPrioritizedTasks; + private boolean mIsShutdown; // The task which is running now. private Runnable mActive; @@ -38,6 +39,7 @@ public class PrioritizedSerialExecutor { public PrioritizedSerialExecutor() { mTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY); mPrioritizedTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY); + mIsShutdown = false; } /** @@ -56,9 +58,11 @@ public class PrioritizedSerialExecutor { */ public void execute(final Runnable r) { synchronized(mLock) { - mTasks.offer(r); - if (mActive == null) { - scheduleNext(); + if (!mIsShutdown) { + mTasks.offer(r); + if (mActive == null) { + scheduleNext(); + } } } } @@ -69,9 +73,11 @@ public class PrioritizedSerialExecutor { */ public void executePrioritized(final Runnable r) { synchronized(mLock) { - mPrioritizedTasks.offer(r); - if (mActive == null) { - scheduleNext(); + if (!mIsShutdown) { + mPrioritizedTasks.offer(r); + if (mActive == null) { + scheduleNext(); + } } } } @@ -123,4 +129,19 @@ public class PrioritizedSerialExecutor { execute(newTask); } } + + public void shutdown() { + synchronized(mLock) { + mIsShutdown = true; + } + } + + public boolean isTerminated() { + synchronized(mLock) { + if (!mIsShutdown) { + return false; + } + return mPrioritizedTasks.isEmpty() && mTasks.isEmpty() && mActive == null; + } + } } diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java index 4c7739a7a..7c6fe93ac 100644 --- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java +++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java @@ -132,6 +132,15 @@ public final class ResizableIntArray { } } + /** + * Shift to the left by elementCount, discarding elementCount pointers at the start. + * @param elementCount how many elements to shift. + */ + public void shift(final int elementCount) { + System.arraycopy(mArray, elementCount, mArray, 0, mLength - elementCount); + mLength -= elementCount; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java index 05f3061a8..ea32a74ff 100644 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java @@ -20,13 +20,13 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; +import com.android.inputmethod.latin.makedict.DictDecoder; 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.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; @@ -125,7 +125,7 @@ public final class UserHistoryDictIOUtils { /** * Reads dictionary from file. */ - public static void readDictionaryBinary(final Ver3DictDecoder dictDecoder, + public static void readDictionaryBinary(final DictDecoder dictDecoder, final OnAddWordListener dict) { final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap(); final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap(); |