Merge "Fix unit test"
diff --git a/java/res/layout/emoji_keyboard_page.xml b/java/res/layout/emoji_keyboard_page.xml
new file mode 100644
index 0000000..e0b752b
--- /dev/null
+++ b/java/res/layout/emoji_keyboard_page.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/emoji_keyboard_scroller"
+    android:clipToPadding="false"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+>
+    <com.android.inputmethod.keyboard.internal.ScrollKeyboardView
+        android:id="@+id/emoji_keyboard_page"
+        android:layoutDirection="ltr"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+</com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier>
diff --git a/java/res/layout/emoji_keyboard_tab_icon.xml b/java/res/layout/emoji_keyboard_tab_icon.xml
new file mode 100644
index 0000000..d79276e
--- /dev/null
+++ b/java/res/layout/emoji_keyboard_tab_icon.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dip"
+    android:layout_weight="1.0"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+/>
diff --git a/java/res/layout/emoji_keyboard_tab_label.xml b/java/res/layout/emoji_keyboard_tab_label.xml
new file mode 100644
index 0000000..62c552d
--- /dev/null
+++ b/java/res/layout/emoji_keyboard_tab_label.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dip"
+    android:layout_weight="1.0"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+/>
diff --git a/java/res/layout/emoji_keyboard_view.xml b/java/res/layout/emoji_keyboard_view.xml
new file mode 100644
index 0000000..ccbcfdc
--- /dev/null
+++ b/java/res/layout/emoji_keyboard_view.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<com.android.inputmethod.keyboard.EmojiKeyboardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/emoji_keyboard_view"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="?attr/emojiKeyboardViewStyle"
+>
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/suggestions_strip_height"
+    >
+        <TabHost
+            android:id="@+id/emoji_category_tabhost"
+            android:layout_width="0dip"
+            android:layout_weight="87.5"
+            android:layout_height="match_parent"
+        >
+            <TabWidget
+                android:id="@android:id/tabs"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@drawable/tab_selected"
+                android:divider="@null"
+                android:tabStripEnabled="true"
+                android:tabStripLeft="@drawable/tab_unselected"
+                android:tabStripRight="@drawable/tab_unselected" />
+            <FrameLayout
+                android:id="@android:id/tabcontent"
+                android:layout_width="0dip"
+                android:layout_height="0dip"
+            >
+                <!-- Empty placeholder that TabHost requires. But we don't use it to actually
+                     display anything. We monitor the tab changes and change the ViewPager.
+                     Similarly the ViewPager swipes are intercepted and passed to the TabHost. -->
+                <View
+                    android:id="@+id/emoji_keyboard_dummy"
+                    android:layout_width="0dip"
+                    android:layout_height="0dip"
+                    android:visibility="gone" />
+            </FrameLayout>
+        </TabHost>
+        <ImageButton
+            android:id="@+id/emoji_keyboard_delete"
+            android:layout_width="0dip"
+            android:layout_weight="12.5"
+            android:layout_height="match_parent"
+            android:src="@drawable/sym_keyboard_delete_holo" />
+    </LinearLayout>
+    <android.support.v4.view.ViewPager
+        android:id="@+id/emoji_keyboard_pager"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+    <LinearLayout
+        android:id="@+id/emoji_action_bar"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/suggestions_strip_height"
+    >
+        <ImageButton
+            android:id="@+id/emoji_keyboard_alphabet"
+            android:layout_width="0dip"
+            android:layout_weight="0.825"
+            android:layout_height="match_parent"
+            android:src="@drawable/ic_ime_light" />
+        <ImageButton
+            android:id="@+id/emoji_keyboard_send"
+            android:layout_width="0dip"
+            android:layout_weight="0.125"
+            android:layout_height="match_parent"
+            android:src="@drawable/sym_keyboard_return_holo" />
+    </LinearLayout>
+</com.android.inputmethod.keyboard.EmojiKeyboardView>
diff --git a/java/res/xml/kbd_emoji_category6.xml b/java/res/xml/kbd_emoji_category6.xml
index a07966b..838f3f5 100644
--- a/java/res/xml/kbd_emoji_category6.xml
+++ b/java/res/xml/kbd_emoji_category6.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:keyLabelSize="60%p"
 >
     <GridRows
         latin:textsArray="@array/emoji_emoticons"
diff --git a/java/res/xml/kbd_emoji_recents.xml b/java/res/xml/kbd_emoji_recents.xml
index 8b4fa95..f56b79a 100644
--- a/java/res/xml/kbd_emoji_recents.xml
+++ b/java/res/xml/kbd_emoji_recents.xml
@@ -22,6 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:keyWidth="@fraction/emoji_keyboard_key_width"
     latin:keyLetterSize="90%p"
+    latin:keyLabelSize="60%p"
 >
     <GridRows
         latin:codesArray="@array/emoji_recents"
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
new file mode 100644
index 0000000..25ff8d0
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TabHost;
+import android.widget.TabHost.OnTabChangeListener;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.internal.RecentsKeyboard;
+import com.android.inputmethod.keyboard.internal.ScrollKeyboardView;
+import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+
+import java.util.HashMap;
+
+/**
+ * View class to implement Emoji keyboards.
+ * The Emoji keyboard consists of group of views {@link R.layout#emoji_keyboard_view}.
+ * <ol>
+ * <li> Emoji category tabs.
+ * <li> Delete button.
+ * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
+ * <li> Back to main keyboard button and enter button.
+ * </ol>
+ * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
+ */
+public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener,
+        ViewPager.OnPageChangeListener, View.OnClickListener,
+        ScrollKeyboardView.OnKeyClickListener {
+    private final int mKeyBackgroundId;
+    private final ColorStateList mTabLabelColor;
+    private final EmojiKeyboardAdapter mEmojiKeyboardAdapter;
+
+    private TabHost mTabHost;
+    private ViewPager mEmojiPager;
+
+    private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
+
+    private int mCurrentCategory = CATEGORY_UNSPECIFIED;
+    private static final int CATEGORY_UNSPECIFIED = -1;
+    private static final int CATEGORY_RECENTS = 0;
+    private static final int CATEGORY_PEOPLE = 1;
+    private static final int CATEGORY_OBJECTS = 2;
+    private static final int CATEGORY_NATURE = 3;
+    private static final int CATEGORY_PLACES = 4;
+    private static final int CATEGORY_SYMBOLS = 5;
+    private static final int CATEGORY_EMOTICONS = 6;
+    private static final HashMap<String, Integer> sCategoryNameToIdMap =
+            CollectionUtils.newHashMap();
+    private static final String[] sCategoryName = {
+        "recents", "people", "objects", "nature", "places", "symbols", "emoticons"
+    };
+    private static final int[] sCategoryIcon = new int[] {
+        R.drawable.ic_emoji_recent_light,
+        R.drawable.ic_emoji_people_light,
+        R.drawable.ic_emoji_objects_light,
+        R.drawable.ic_emoji_nature_light,
+        R.drawable.ic_emoji_places_light,
+        R.drawable.ic_emoji_symbols_light,
+        0
+    };
+    private static final String[] sCategoryLabel = {
+        null, null, null, null, null, null,
+        ":-)"
+    };
+    private static final int[] sCategoryElementId = {
+        KeyboardId.ELEMENT_EMOJI_RECENTS,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY1,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY2,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY3,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY4,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY5,
+        KeyboardId.ELEMENT_EMOJI_CATEGORY6,
+    };
+
+    public EmojiKeyboardView(final Context context, final AttributeSet attrs) {
+        this(context, attrs, R.attr.emojiKeyboardViewStyle);
+    }
+
+    public EmojiKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
+        super(context, attrs, defStyle);
+        final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
+                R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+        mKeyBackgroundId = keyboardViewAttr.getResourceId(
+                R.styleable.KeyboardView_keyBackground, 0);
+        keyboardViewAttr.recycle();
+        final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs,
+                R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView);
+        mTabLabelColor = emojiKeyboardViewAttr.getColorStateList(
+                R.styleable.EmojiKeyboardView_emojiTabLabelColor);
+        emojiKeyboardViewAttr.recycle();
+        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+                context, null /* editorInfo */);
+        final Resources res = context.getResources();
+        builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
+        // TODO: Make Keyboard height variable.
+        builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
+                (int)(ResourceUtils.getDefaultKeyboardHeight(res)
+                        - res.getDimension(R.dimen.suggestions_strip_height)));
+        builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */);
+        final KeyboardLayoutSet layoutSet = builder.build();
+        mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(layoutSet, this);
+        // TODO: Save/restore recent keys from/to preferences.
+    }
+
+    @Override
+    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final Resources res = getContext().getResources();
+        // The main keyboard expands to the entire this {@link KeyboardView}.
+        final int width = ResourceUtils.getDefaultKeyboardWidth(res)
+                + getPaddingLeft() + getPaddingRight();
+        final int height = ResourceUtils.getDefaultKeyboardHeight(res)
+                + res.getDimensionPixelSize(R.dimen.suggestions_strip_height)
+                + getPaddingTop() + getPaddingBottom();
+        setMeasuredDimension(width, height);
+    }
+
+    private void addTab(final TabHost host, final int category) {
+        final String tabId = sCategoryName[category];
+        sCategoryNameToIdMap.put(tabId, category);
+        final TabHost.TabSpec tspec = host.newTabSpec(tabId);
+        tspec.setContent(R.id.emoji_keyboard_dummy);
+        if (sCategoryIcon[category] != 0) {
+            final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
+                    R.layout.emoji_keyboard_tab_icon, null);
+            iconView.setImageResource(sCategoryIcon[category]);
+            tspec.setIndicator(iconView);
+        }
+        if (sCategoryLabel[category] != null) {
+            final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate(
+                    R.layout.emoji_keyboard_tab_label, null);
+            textView.setText(sCategoryLabel[category]);
+            textView.setTextColor(mTabLabelColor);
+            textView.setBackgroundResource(mKeyBackgroundId);
+            tspec.setIndicator(textView);
+        }
+        host.addTab(tspec);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
+        mTabHost.setup();
+        addTab(mTabHost, CATEGORY_RECENTS);
+        addTab(mTabHost, CATEGORY_PEOPLE);
+        addTab(mTabHost, CATEGORY_OBJECTS);
+        addTab(mTabHost, CATEGORY_NATURE);
+        addTab(mTabHost, CATEGORY_PLACES);
+        addTab(mTabHost, CATEGORY_SYMBOLS);
+        addTab(mTabHost, CATEGORY_EMOTICONS);
+        mTabHost.setOnTabChangedListener(this);
+        mTabHost.getTabWidget().setStripEnabled(true);
+
+        mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
+        mEmojiPager.setAdapter(mEmojiKeyboardAdapter);
+        mEmojiPager.setOnPageChangeListener(this);
+        mEmojiPager.setOffscreenPageLimit(0);
+        final ViewGroup.LayoutParams lp = mEmojiPager.getLayoutParams();
+        final Resources res = getResources();
+        lp.height = ResourceUtils.getDefaultKeyboardHeight(res)
+                - res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
+        mEmojiPager.setLayoutParams(lp);
+
+        // TODO: Record current category.
+        final int category = CATEGORY_PEOPLE;
+        setCurrentCategory(category, true /* force */);
+
+        // TODO: Implement auto repeat, using View.OnTouchListener?
+        final View deleteKey = findViewById(R.id.emoji_keyboard_delete);
+        deleteKey.setBackgroundResource(mKeyBackgroundId);
+        deleteKey.setTag(Constants.CODE_DELETE);
+        deleteKey.setOnClickListener(this);
+        final View alphabetKey = findViewById(R.id.emoji_keyboard_alphabet);
+        alphabetKey.setBackgroundResource(mKeyBackgroundId);
+        alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
+        alphabetKey.setOnClickListener(this);
+        final View sendKey = findViewById(R.id.emoji_keyboard_send);
+        sendKey.setBackgroundResource(mKeyBackgroundId);
+        sendKey.setTag(Constants.CODE_ENTER);
+        sendKey.setOnClickListener(this);
+    }
+
+    @Override
+    public void onTabChanged(final String tabId) {
+        final int category = sCategoryNameToIdMap.get(tabId);
+        setCurrentCategory(category, false /* force */);
+    }
+
+
+    @Override
+    public void onPageSelected(final int position) {
+        setCurrentCategory(position, false /* force */);
+    }
+
+    @Override
+    public void onPageScrollStateChanged(final int state) {
+        // Ignore this message. Only want the actual page selected.
+    }
+
+    @Override
+    public void onPageScrolled(final int position, final float positionOffset,
+            final int positionOffsetPixels) {
+        // Ignore this message. Only want the actual page selected.
+    }
+
+    @Override
+    public void onClick(final View v) {
+        if (v.getTag() instanceof Integer) {
+            final int code = (Integer)v.getTag();
+            registerCode(code);
+            return;
+        }
+    }
+
+    private void registerCode(final int code) {
+        mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */);
+        mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE);
+        mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
+    }
+
+    @Override
+    public void onKeyClick(final Key key) {
+        mEmojiKeyboardAdapter.addRecentKey(key);
+        final int code = key.getCode();
+        if (code == Constants.CODE_OUTPUT_TEXT) {
+            mKeyboardActionListener.onTextInput(key.getOutputText());
+            return;
+        }
+        registerCode(code);
+    }
+
+    public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+        // TODO:
+    }
+
+    public void setKeyboardActionListener(final KeyboardActionListener listener) {
+        mKeyboardActionListener = listener;
+    }
+
+    private void setCurrentCategory(final int category, final boolean force) {
+        if (mCurrentCategory == category && !force) {
+            return;
+        }
+
+        mCurrentCategory = category;
+        if (force || mEmojiPager.getCurrentItem() != category) {
+            mEmojiPager.setCurrentItem(category, true /* smoothScroll */);
+        }
+        if (force || mTabHost.getCurrentTab() != category) {
+            mTabHost.setCurrentTab(category);
+        }
+        // TODO: Record current category
+    }
+
+    private static class EmojiKeyboardAdapter extends PagerAdapter {
+        private final ScrollKeyboardView.OnKeyClickListener mListener;
+        private final KeyboardLayoutSet mLayoutSet;
+        private final RecentsKeyboard mRecentsKeyboard;
+        private final SparseArray<ScrollKeyboardView> mActiveKeyboardView =
+                CollectionUtils.newSparseArray();
+        private int mActivePosition = CATEGORY_UNSPECIFIED;
+
+        public EmojiKeyboardAdapter(final KeyboardLayoutSet layoutSet,
+                final ScrollKeyboardView.OnKeyClickListener listener) {
+            mListener = listener;
+            mLayoutSet = layoutSet;
+            mRecentsKeyboard = new RecentsKeyboard(
+                    layoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS));
+        }
+
+        public void addRecentKey(final Key key) {
+            if (mActivePosition == CATEGORY_RECENTS) {
+                return;
+            }
+            mRecentsKeyboard.addRecentKey(key);
+            final KeyboardView recentKeyboardView = mActiveKeyboardView.get(CATEGORY_RECENTS);
+            if (recentKeyboardView != null) {
+                recentKeyboardView.invalidateAllKeys();
+            }
+        }
+
+        @Override
+        public int getCount() {
+            return sCategoryName.length;
+        }
+
+        @Override
+        public void setPrimaryItem(final View container, final int position, final Object object) {
+            if (mActivePosition == position) {
+                return;
+            }
+            final ScrollKeyboardView oldKeyboardView = mActiveKeyboardView.get(mActivePosition);
+            if (oldKeyboardView != null) {
+                oldKeyboardView.releaseCurrentKey();
+                oldKeyboardView.deallocateMemory();
+            }
+            mActivePosition = position;
+        }
+
+        @Override
+        public Object instantiateItem(final ViewGroup container, final int position) {
+            final int elementId = sCategoryElementId[position];
+            final Keyboard keyboard = (elementId == KeyboardId.ELEMENT_EMOJI_RECENTS)
+                    ? mRecentsKeyboard : mLayoutSet.getKeyboard(elementId);
+            final LayoutInflater inflater = LayoutInflater.from(container.getContext());
+            final View view = inflater.inflate(
+                    R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
+            final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById(
+                    R.id.emoji_keyboard_page);
+            keyboardView.setKeyboard(keyboard);
+            keyboardView.setOnKeyClickListener(mListener);
+            final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById(
+                    R.id.emoji_keyboard_scroller);
+            keyboardView.setScrollView(scrollView);
+            container.addView(view);
+            mActiveKeyboardView.put(position, keyboardView);
+            return view;
+        }
+
+        @Override
+        public boolean isViewFromObject(final View view, final Object object) {
+            return view == object;
+        }
+
+        @Override
+        public void destroyItem(final ViewGroup container, final int position,
+                final Object object) {
+            final ScrollKeyboardView keyboardView = mActiveKeyboardView.get(position);
+            if (keyboardView != null) {
+                keyboardView.deallocateMemory();
+                mActiveKeyboardView.remove(position);
+            }
+            container.removeView(keyboardView);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index eb19ef9..e8b0657 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -21,7 +21,6 @@
 
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils;
 import com.android.inputmethod.latin.settings.NativeSuggestOptions;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
@@ -120,15 +119,16 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
-                0 /* sessionId */);
+                additionalFeaturesOptions, 0 /* sessionId */);
     }
 
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords, final int sessionId) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+            final int sessionId) {
         if (!isValidDictionary()) return null;
 
         Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
@@ -148,8 +148,7 @@
         final InputPointers ips = composer.getInputPointers();
         final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
         mNativeSuggestOptions.setIsGesture(isGesture);
-        mNativeSuggestOptions.setAdditionalFeaturesOptions(
-                AdditionalFeaturesSettingUtils.getAdditionalNativeSuggestOptions());
+        mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
         // proximityInfo and/or prevWordForBigrams may not be null.
         final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
                 getTraverseSession(sessionId).getSession(), ips.getXCoordinates(),
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index d9ded7c..8a3a884 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -72,20 +72,23 @@
      * @param prevWord the previous word, or null if none
      * @param proximityInfo the object for key proximity. May be ignored by some implementations.
      * @param blockOffensiveWords whether to block potentially offensive words
+     * @param additionalFeaturesOptions options about additional features used for the suggestion.
      * @return the list of suggestions (possibly null if none)
      */
     // TODO: pass more context than just the previous word, to enable better suggestions (n-gram
     // and more)
     abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords);
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions);
 
     // The default implementation of this method ignores sessionId.
     // Subclasses that want to use sessionId need to override this method.
     public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords, final int sessionId) {
-        return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords);
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+            final int sessionId) {
+        return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords,
+                additionalFeaturesOptions);
     }
 
     /**
@@ -156,7 +159,7 @@
         @Override
         public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
                 final String prevWord, final ProximityInfo proximityInfo,
-                final boolean blockOffensiveWords) {
+                final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
             return null;
         }
 
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index d05bb1e..bf07514 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -58,18 +58,18 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
         if (dictionaries.isEmpty()) return null;
         // To avoid creating unnecessary objects, we get the list out of the first
         // dictionary and add the rest to it if not null, hence the get(0)
         ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
-                prevWord, proximityInfo, blockOffensiveWords);
+                prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
         if (null == suggestions) suggestions = CollectionUtils.newArrayList();
         final int length = dictionaries.size();
         for (int i = 1; i < length; ++ i) {
             final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
-                    prevWord, proximityInfo, blockOffensiveWords);
+                    prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
             if (null != sugg) suggestions.addAll(sugg);
         }
         return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index a97e053..5a453dd 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -92,7 +92,7 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            boolean blockOffensiveWords) {
+            boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         // This class doesn't support suggestion.
         return null;
     }
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 939c2a0..b92283c 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -272,19 +272,19 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         asyncReloadDictionaryIfRequired();
         // Write lock because getSuggestions in native updates session status.
         if (mLocalDictionaryController.writeLock().tryLock()) {
             try {
                 final ArrayList<SuggestedWordInfo> inMemDictSuggestion =
                         mDictionaryWriter.getSuggestions(composer, prevWord, proximityInfo,
-                                blockOffensiveWords);
+                                blockOffensiveWords, additionalFeaturesOptions);
                 // TODO: Remove checking mIsUpdatable and use native suggestion.
                 if (mBinaryDictionary != null && !mIsUpdatable) {
                     final ArrayList<SuggestedWordInfo> binarySuggestion =
                             mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
-                                    blockOffensiveWords);
+                                    blockOffensiveWords, additionalFeaturesOptions);
                     if (inMemDictSuggestion == null) {
                         return binarySuggestion;
                     } else if (binarySuggestion == null) {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index f5fa5d0..342dcfc 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -210,7 +210,7 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         if (composer.size() > 1) {
             if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
                 return null;
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 85001c3..de4baf9 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -2323,6 +2323,7 @@
         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
         // should just skip whitespace if any, so 1.
         final SettingsValues currentSettings = mSettings.getCurrent();
+        final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
         final String prevWord;
         if (currentSettings.mCurrentLanguageHasSpaces) {
             // If we are typing in a language with spaces we can just look up the previous
@@ -2335,7 +2336,7 @@
         }
         return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive,
-                currentSettings.mCorrectionEnabled, sessionId);
+                currentSettings.mCorrectionEnabled, additionalFeaturesOptions, sessionId);
     }
 
     private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index c8a151a..826387a 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -214,21 +214,23 @@
     public SuggestedWords getSuggestedWords(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
             final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
-            final int sessionId) {
+            final int[] additionalFeaturesOptions, final int sessionId) {
         LatinImeLogger.onStartSuggestion(prevWordForBigram);
         if (wordComposer.isBatchMode()) {
             return getSuggestedWordsForBatchInput(
-                    wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId);
+                    wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords,
+                    additionalFeaturesOptions, sessionId);
         } else {
             return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
-                    blockOffensiveWords, isCorrectionEnabled);
+                    blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions);
         }
     }
 
     // Retrieves suggestions for the typing input.
     private SuggestedWords getSuggestedWordsForTypingInput(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords, final boolean isCorrectionEnabled) {
+            final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
+            final int[] additionalFeaturesOptions) {
         final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
@@ -252,7 +254,8 @@
         for (final String key : mDictionaries.keySet()) {
             final Dictionary dictionary = mDictionaries.get(key);
             suggestionsSet.addAll(dictionary.getSuggestions(
-                    wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords));
+                    wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords,
+                    additionalFeaturesOptions));
         }
 
         final String whitelistedWord;
@@ -343,7 +346,8 @@
     // Retrieves suggestions for the batch input.
     private SuggestedWords getSuggestedWordsForBatchInput(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords, final int sessionId) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+            final int sessionId) {
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
 
@@ -357,7 +361,8 @@
             }
             final Dictionary dictionary = mDictionaries.get(key);
             suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
-                    prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId));
+                    prevWordForBigram, proximityInfo, blockOffensiveWords,
+                    additionalFeaturesOptions, sessionId));
         }
 
         for (SuggestedWordInfo wordInfo : suggestionsSet) {
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 92f96c0..67ef538 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -34,9 +34,10 @@
     @Override
     public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
             final String prevWordForBigrams, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         syncReloadDictionaryIfRequired();
-        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+                additionalFeaturesOptions);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 33fe896..bea5223 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -37,9 +37,10 @@
     @Override
     public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
             final String prevWordForBigrams, final ProximityInfo proximityInfo,
-            final boolean blockOffensiveWords) {
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         syncReloadDictionaryIfRequired();
-        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+        return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+                additionalFeaturesOptions);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
index 7f4f5e7..d446606 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
@@ -147,9 +147,9 @@
     @Override
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final String prevWord, final ProximityInfo proximityInfo,
-            boolean blockOffensiveWords) {
+            boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
         return mExpandableDictionary.getSuggestions(composer, prevWord, proximityInfo,
-                blockOffensiveWords);
+                blockOffensiveWords, additionalFeaturesOptions);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
index 139f5e2..6543003 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -40,8 +40,4 @@
             final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
         // do nothing.
     }
-
-    public static int[] getAdditionalNativeSuggestOptions() {
-        return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
index 878c505..cd726c9 100644
--- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
+++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
@@ -34,6 +34,9 @@
     }
 
     public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+        if (additionalOptions == null) {
+            return;
+        }
         for (int i = 0; i < additionalOptions.length; i++) {
             setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
         }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 6719e98..69f9a46 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -301,12 +301,14 @@
                 // TODO: make a spell checker option to block offensive words or not
                 final ArrayList<SuggestedWordInfo> suggestions =
                         dictInfo.mDictionary.getSuggestions(composer, prevWord,
-                                dictInfo.getProximityInfo(),
-                                true /* blockOffensiveWords */);
-                for (final SuggestedWordInfo suggestion : suggestions) {
-                    final String suggestionStr = suggestion.mWord;
-                    suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
-                            suggestionStr.length(), suggestion.mScore);
+                                dictInfo.getProximityInfo(), true /* blockOffensiveWords */,
+                                null /* additionalFeaturesOptions */);
+                if (suggestions != null) {
+                    for (final SuggestedWordInfo suggestion : suggestions) {
+                        final String suggestionStr = suggestion.mWord;
+                        suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+                                suggestionStr.length(), suggestion.mScore);
+                    }
                 }
                 isInDict = isInDictForAnyCapitalization(dictInfo.mDictionary, text, capitalizeType);
             } finally {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index ac8f687..a0aed28 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -52,7 +52,7 @@
                 @Override
                 public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
                         final String prevWord, final ProximityInfo proximityInfo,
-                        final boolean blockOffensiveWords) {
+                        final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
                     return noSuggestions;
                 }
                 @Override