Merge "Restrict the permission of dict files created on device."
diff --git a/java/src/com/android/inputmethod/event/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java
index 27b8c14..da0228b 100644
--- a/java/src/com/android/inputmethod/event/MyanmarReordering.java
+++ b/java/src/com/android/inputmethod/event/MyanmarReordering.java
@@ -207,9 +207,33 @@
                 return clearAndGetResultingEvent(newEvent);
             }
         } else if (Constants.CODE_DELETE == newEvent.mKeyCode) {
-            if (mCurrentEvents.size() > 0) {
-                mCurrentEvents.remove(mCurrentEvents.size() - 1);
-                return null;
+            final Event lastEvent = getLastEvent();
+            final int eventSize = mCurrentEvents.size();
+            if (null != lastEvent) {
+                if (VOWEL_E == lastEvent.mCodePoint) {
+                    // We have a VOWEL_E at the end. There are four cases.
+                    // - The vowel is the only code point in the buffer. Remove it.
+                    // - The vowel is preceded by a ZWNJ. Remove both vowel E and ZWNJ.
+                    // - The vowel is preceded by a consonant/medial, remove the consonant/medial.
+                    // - In all other cases, it's strange, so just remove the last code point.
+                    if (eventSize <= 1) {
+                        mCurrentEvents.clear();
+                    } else { // eventSize >= 2
+                        final int previousCodePoint = mCurrentEvents.get(eventSize - 2).mCodePoint;
+                        if (previousCodePoint == ZERO_WIDTH_NON_JOINER) {
+                            mCurrentEvents.remove(eventSize - 1);
+                            mCurrentEvents.remove(eventSize - 2);
+                        } else if (isConsonantOrMedial(previousCodePoint)) {
+                            mCurrentEvents.remove(eventSize - 2);
+                        } else {
+                            mCurrentEvents.remove(eventSize - 1);
+                        }
+                    }
+                    return null;
+                } else if (eventSize > 0) {
+                    mCurrentEvents.remove(eventSize - 1);
+                    return null;
+                }
             }
         }
         // This character is not part of the combining scheme, so we should reset everything.
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
index 4ef9e22..9922f90 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
@@ -24,7 +24,8 @@
 import android.util.AttributeSet;
 import android.widget.LinearLayout;
 
-public class EmojiCategoryPageIndicatorView extends LinearLayout {
+//TODO: Move this class to com.android.inputmethod.emoji package.
+public final class EmojiCategoryPageIndicatorView extends LinearLayout {
     private static final float BOTTOM_MARGIN_RATIO = 1.0f;
     private final Paint mPaint = new Paint();
     private int mCategoryPageSize = 0;
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
index d8b5758..d5f166c 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
@@ -19,50 +19,36 @@
 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.Color;
-import android.graphics.Rect;
-import android.os.Build;
 import android.os.CountDownTimer;
 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.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 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.DynamicGridKeyboard;
+import com.android.inputmethod.keyboard.internal.EmojiCategory;
 import com.android.inputmethod.keyboard.internal.EmojiLayoutParams;
 import com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView;
+import com.android.inputmethod.keyboard.internal.EmojiPalettesAdapter;
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
 import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
 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.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -76,11 +62,10 @@
  * </ol>
  * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
  */
+// TODO: Move this class to com.android.inputmethod.emoji package.
 public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
         ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener,
         EmojiPageKeyboardView.OnKeyEventListener {
-    static final String TAG = EmojiPalettesView.class.getSimpleName();
-    private static final boolean DEBUG_PAGER = false;
     private final int mKeyBackgroundId;
     private final int mEmojiFunctionalKeyBackgroundId;
     private final ColorStateList mTabLabelColor;
@@ -97,317 +82,6 @@
 
     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
 
-    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 = {
-                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[] sAccessibilityDescriptionResourceIdsForCategories = {
-                R.string.spoken_descrption_emoji_category_recents,
-                R.string.spoken_descrption_emoji_category_people,
-                R.string.spoken_descrption_emoji_category_objects,
-                R.string.spoken_descrption_emoji_category_nature,
-                R.string.spoken_descrption_emoji_category_places,
-                R.string.spoken_descrption_emoji_category_symbols,
-                R.string.spoken_descrption_emoji_category_emoticons };
-        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 Resources mRes;
-        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;
-            mRes = res;
-            mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_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
-                    || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie")
-                    || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) {
-                addShownCategoryId(CATEGORY_ID_PEOPLE);
-                addShownCategoryId(CATEGORY_ID_OBJECTS);
-                addShownCategoryId(CATEGORY_ID_NATURE);
-                addShownCategoryId(CATEGORY_ID_PLACES);
-                mCurrentCategoryId =
-                        Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_PEOPLE);
-            } else {
-                mCurrentCategoryId =
-                        Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_SYMBOLS);
-            }
-            addShownCategoryId(CATEGORY_ID_SYMBOLS);
-            addShownCategoryId(CATEGORY_ID_EMOTICONS);
-            getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */)
-                    .loadRecentKeys(mCategoryKeyboardMap.values());
-        }
-
-        private void addShownCategoryId(final 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(final int categoryId, final int categoryPageId) {
-            return sCategoryName[categoryId] + "-" + categoryPageId;
-        }
-
-        public int getCategoryId(final String name) {
-            final String[] strings = name.split("-");
-            return mCategoryNameToIdMap.get(strings[0]);
-        }
-
-        public int getCategoryIcon(final int categoryId) {
-            return sCategoryIcon[categoryId];
-        }
-
-        public String getCategoryLabel(final int categoryId) {
-            return sCategoryLabel[categoryId];
-        }
-
-        public String getAccessibilityDescription(final int categoryId) {
-            return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]);
-        }
-
-        public ArrayList<CategoryProperties> getShownCategories() {
-            return mShownCategories;
-        }
-
-        public int getCurrentCategoryId() {
-            return mCurrentCategoryId;
-        }
-
-        public int getCurrentCategoryPageSize() {
-            return getCategoryPageSize(mCurrentCategoryId);
-        }
-
-        public int getCategoryPageSize(final int categoryId) {
-            for (final CategoryProperties prop : mShownCategories) {
-                if (prop.mCategoryId == categoryId) {
-                    return prop.mPageCount;
-                }
-            }
-            Log.w(TAG, "Invalid category id: " + categoryId);
-            // Should not reach here.
-            return 0;
-        }
-
-        public void setCurrentCategoryId(final int categoryId) {
-            mCurrentCategoryId = categoryId;
-            Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
-        }
-
-        public void setCurrentCategoryPageId(final int id) {
-            mCurrentCategoryPageId = id;
-        }
-
-        public int getCurrentCategoryPageId() {
-            return mCurrentCategoryPageId;
-        }
-
-        public void saveLastTypedCategoryPage() {
-            Settings.writeLastTypedEmojiCategoryPageId(
-                    mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
-        }
-
-        public boolean isInRecentTab() {
-            return mCurrentCategoryId == CATEGORY_ID_RECENTS;
-        }
-
-        public int getTabIdFromCategoryId(final 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(final int categoryId) {
-            final int lastSavedCategoryPageId =
-                    Settings.readLastTypedEmojiCategoryPageId(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(final int categoryId) {
-            final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
-            return (keyboard.getSortedKeys().size() - 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(final int position) {
-            int sum = 0;
-            for (final 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(final int position) {
-            final Pair<Integer, Integer> categoryAndId =
-                    getCategoryIdAndPageIdFromPagePosition(position);
-            if (categoryAndId != null) {
-                return getKeyboard(categoryAndId.first, categoryAndId.second);
-            }
-            return null;
-        }
-
-        private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
-            return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
-        }
-
-        public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
-            synchronized (mCategoryKeyboardMap) {
-                final Long categotyKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id);
-                if (mCategoryKeyboardMap.containsKey(categotyKeyboardMapKey)) {
-                    return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
-                }
-
-                if (categoryId == CATEGORY_ID_RECENTS) {
-                    final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
-                            mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
-                            mMaxPageKeyCount, categoryId);
-                    mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd);
-                    return kbd;
-                }
-
-                final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
-                final Key[][] sortedKeys = sortKeysIntoPages(
-                        keyboard.getSortedKeys(), mMaxPageKeyCount);
-                for (int pageId = 0; pageId < sortedKeys.length; ++pageId) {
-                    final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
-                            mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
-                            mMaxPageKeyCount, categoryId);
-                    for (final Key emojiKey : sortedKeys[pageId]) {
-                        if (emojiKey == null) {
-                            break;
-                        }
-                        tempKeyboard.addKeyLast(emojiKey);
-                    }
-                    mCategoryKeyboardMap.put(
-                            getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard);
-                }
-                return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
-            }
-        }
-
-        public int getTotalPageCountOfAllCategories() {
-            int sum = 0;
-            for (CategoryProperties properties : mShownCategories) {
-                sum += properties.mPageCount;
-            }
-            return sum;
-        }
-
-        private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() {
-            @Override
-            public int compare(final Key lhs, final 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;
-            }
-        };
-
-        private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) {
-            final ArrayList<Key> keys = CollectionUtils.newArrayList(inKeys);
-            Collections.sort(keys, EMOJI_KEY_COMPARATOR);
-            final int pageCount = (keys.size() - 1) / maxPageCount + 1;
-            final Key[][] retval = new Key[pageCount][maxPageCount];
-            for (int i = 0; i < keys.size(); ++i) {
-                retval[i / maxPageCount][i % maxPageCount] = keys.get(i);
-            }
-            return retval;
-        }
-    }
-
     private final EmojiCategory mEmojiCategory;
 
     public EmojiPalettesView(final Context context, final AttributeSet attrs) {
@@ -481,7 +155,8 @@
     protected void onFinishInflate() {
         mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
         mTabHost.setup();
-        for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) {
+        for (final EmojiCategory.CategoryProperties properties
+                : mEmojiCategory.getShownCategories()) {
             addTab(mTabHost, properties.mCategoryId);
         }
         mTabHost.setOnTabChangedListener(this);
@@ -675,9 +350,6 @@
 
     public void startEmojiPalettes(final String switchToAlphaLabel,
             final KeyVisualAttributes keyVisualAttr) {
-        if (DEBUG_PAGER) {
-            Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition);
-        }
         final KeyDrawParams params = new KeyDrawParams();
         params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr);
         setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params);
@@ -687,9 +359,6 @@
     }
 
     public void stopEmojiPalettes() {
-        if (DEBUG_PAGER) {
-            Log.d(TAG, "deallocate emoji palettes memory");
-        }
         mEmojiPalettesAdapter.flushPendingRecentKeys();
         mEmojiPager.setAdapter(null);
     }
@@ -714,7 +383,7 @@
             return;
         }
 
-        if (oldCategoryId == CATEGORY_ID_RECENTS) {
+        if (oldCategoryId == EmojiCategory.ID_RECENTS) {
             // Needs to save pending updates for recent keys when we get out of the recents
             // category because we don't want to move the recent emojis around while the user
             // is in the recents category.
@@ -733,124 +402,11 @@
         }
     }
 
-    private static class EmojiPalettesAdapter extends PagerAdapter {
-        private final EmojiPageKeyboardView.OnKeyEventListener mListener;
-        private final DynamicGridKeyboard mRecentsKeyboard;
-        private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews =
-                CollectionUtils.newSparseArray();
-        private final EmojiCategory mEmojiCategory;
-        private int mActivePosition = 0;
-
-        public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
-                final EmojiPageKeyboardView.OnKeyEventListener listener) {
-            mEmojiCategory = emojiCategory;
-            mListener = listener;
-            mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0);
-        }
-
-        public void flushPendingRecentKeys() {
-            mRecentsKeyboard.flushPendingRecentKeys();
-            final KeyboardView recentKeyboardView =
-                    mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
-            if (recentKeyboardView != null) {
-                recentKeyboardView.invalidateAllKeys();
-            }
-        }
-
-        public void addRecentKey(final Key key) {
-            if (mEmojiCategory.isInRecentTab()) {
-                mRecentsKeyboard.addPendingKey(key);
-                return;
-            }
-            mRecentsKeyboard.addKeyFirst(key);
-            final KeyboardView recentKeyboardView =
-                    mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
-            if (recentKeyboardView != null) {
-                recentKeyboardView.invalidateAllKeys();
-            }
-        }
-
-        public void onPageScrolled() {
-            // Make sure the delayed key-down event (highlight effect and haptic feedback) will be
-            // canceled.
-            final EmojiPageKeyboardView currentKeyboardView =
-                    mActiveKeyboardViews.get(mActivePosition);
-            if (currentKeyboardView != null) {
-                currentKeyboardView.releaseCurrentKey();
-            }
-        }
-
-        @Override
-        public int getCount() {
-            return mEmojiCategory.getTotalPageCountOfAllCategories();
-        }
-
-        @Override
-        public void setPrimaryItem(final ViewGroup container, final int position,
-                final Object object) {
-            if (mActivePosition == position) {
-                return;
-            }
-            final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
-            if (oldKeyboardView != null) {
-                oldKeyboardView.releaseCurrentKey();
-                oldKeyboardView.deallocateMemory();
-            }
-            mActivePosition = position;
-        }
-
-        @Override
-        public Object instantiateItem(final ViewGroup container, final int position) {
-            if (DEBUG_PAGER) {
-                Log.d(TAG, "instantiate item: " + position);
-            }
-            final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
-            if (oldKeyboardView != null) {
-                oldKeyboardView.deallocateMemory();
-                // This may be redundant but wanted to be safer..
-                mActiveKeyboardViews.remove(position);
-            }
-            final Keyboard keyboard =
-                    mEmojiCategory.getKeyboardFromPagePosition(position);
-            final LayoutInflater inflater = LayoutInflater.from(container.getContext());
-            final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate(
-                    R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
-            keyboardView.setKeyboard(keyboard);
-            keyboardView.setOnKeyEventListener(mListener);
-            container.addView(keyboardView);
-            mActiveKeyboardViews.put(position, keyboardView);
-            return keyboardView;
-        }
-
-        @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) {
-            if (DEBUG_PAGER) {
-                Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
-            }
-            final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position);
-            if (keyboardView != null) {
-                keyboardView.deallocateMemory();
-                mActiveKeyboardViews.remove(position);
-            }
-            if (object instanceof View) {
-                container.removeView((View)object);
-            } else {
-                Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
-            }
-        }
-    }
-
     private static class DeleteKeyOnTouchListener implements OnTouchListener {
-        private static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30);
-        private final int mDeleteKeyPressedBackgroundColor;
-        private final long mKeyRepeatStartTimeout;
-        private final long mKeyRepeatInterval;
+        static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30);
+        final int mDeleteKeyPressedBackgroundColor;
+        final long mKeyRepeatStartTimeout;
+        final long mKeyRepeatInterval;
 
         public DeleteKeyOnTouchListener(Context context) {
             final Resources res = context.getResources();
@@ -953,7 +509,7 @@
         }
 
         // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal.
-        private void onKeyRepeat() {
+        void onKeyRepeat() {
             switch (mState) {
             case KEY_REPEAT_STATE_INITIALIZED:
                 // Basically this should not happen.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
index 67a2227..a4879b8 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -20,7 +20,6 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.inputmethod.keyboard.EmojiPalettesView;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.settings.Settings;
@@ -36,6 +35,7 @@
 /**
  * This is a Keyboard class where you can add keys dynamically shown in a grid layout
  */
+// TODO: Move this class to com.android.inputmethod.emoji package.
 public class DynamicGridKeyboard extends Keyboard {
     private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
     private static final int TEMPLATE_KEY_CODE_0 = 0x30;
@@ -62,7 +62,7 @@
         mVerticalStep = key0.getHeight() + mVerticalGap;
         mColumnsNum = mBaseWidth / mHorizontalStep;
         mMaxKeyCount = maxKeyCount;
-        mIsRecents = categoryId == EmojiPalettesView.CATEGORY_ID_RECENTS;
+        mIsRecents = categoryId == EmojiCategory.ID_RECENTS;
         mPrefs = prefs;
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiCategory.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiCategory.java
new file mode 100644
index 0000000..10bd621
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiCategory.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+// TODO: Move this class to com.android.inputmethod.emoji package.
+public final class EmojiCategory {
+    private final String TAG = EmojiCategory.class.getSimpleName();
+
+    private static final int ID_UNSPECIFIED = -1;
+    public static final int ID_RECENTS = 0;
+    private static final int ID_PEOPLE = 1;
+    private static final int ID_OBJECTS = 2;
+    private static final int ID_NATURE = 3;
+    private static final int ID_PLACES = 4;
+    private static final int ID_SYMBOLS = 5;
+    private static final int ID_EMOTICONS = 6;
+
+    public final class CategoryProperties {
+        public final int mCategoryId;
+        public final int mPageCount;
+        public CategoryProperties(final int categoryId, final int pageCount) {
+            mCategoryId = categoryId;
+            mPageCount = pageCount;
+        }
+    }
+
+    private static final String[] sCategoryName = {
+            "recents",
+            "people",
+            "objects",
+            "nature",
+            "places",
+            "symbols",
+            "emoticons" };
+
+    private static final int[] sCategoryIcon = {
+            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[] sAccessibilityDescriptionResourceIdsForCategories = {
+            R.string.spoken_descrption_emoji_category_recents,
+            R.string.spoken_descrption_emoji_category_people,
+            R.string.spoken_descrption_emoji_category_objects,
+            R.string.spoken_descrption_emoji_category_nature,
+            R.string.spoken_descrption_emoji_category_places,
+            R.string.spoken_descrption_emoji_category_symbols,
+            R.string.spoken_descrption_emoji_category_emoticons };
+
+    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 Resources mRes;
+    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 = EmojiCategory.ID_UNSPECIFIED;
+    private int mCurrentCategoryPageId = 0;
+
+    public EmojiCategory(final SharedPreferences prefs, final Resources res,
+            final KeyboardLayoutSet layoutSet) {
+        mPrefs = prefs;
+        mRes = res;
+        mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count);
+        mLayoutSet = layoutSet;
+        for (int i = 0; i < sCategoryName.length; ++i) {
+            mCategoryNameToIdMap.put(sCategoryName[i], i);
+        }
+        addShownCategoryId(EmojiCategory.ID_RECENTS);
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
+                || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie")
+                || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) {
+            addShownCategoryId(EmojiCategory.ID_PEOPLE);
+            addShownCategoryId(EmojiCategory.ID_OBJECTS);
+            addShownCategoryId(EmojiCategory.ID_NATURE);
+            addShownCategoryId(EmojiCategory.ID_PLACES);
+            mCurrentCategoryId =
+                    Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_PEOPLE);
+        } else {
+            mCurrentCategoryId =
+                    Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_SYMBOLS);
+        }
+        addShownCategoryId(EmojiCategory.ID_SYMBOLS);
+        addShownCategoryId(EmojiCategory.ID_EMOTICONS);
+        getKeyboard(EmojiCategory.ID_RECENTS, 0 /* cagetoryPageId */)
+                .loadRecentKeys(mCategoryKeyboardMap.values());
+    }
+
+    private void addShownCategoryId(final 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(final int categoryId, final int categoryPageId) {
+        return sCategoryName[categoryId] + "-" + categoryPageId;
+    }
+
+    public int getCategoryId(final String name) {
+        final String[] strings = name.split("-");
+        return mCategoryNameToIdMap.get(strings[0]);
+    }
+
+    public int getCategoryIcon(final int categoryId) {
+        return sCategoryIcon[categoryId];
+    }
+
+    public String getCategoryLabel(final int categoryId) {
+        return sCategoryLabel[categoryId];
+    }
+
+    public String getAccessibilityDescription(final int categoryId) {
+        return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]);
+    }
+
+    public ArrayList<CategoryProperties> getShownCategories() {
+        return mShownCategories;
+    }
+
+    public int getCurrentCategoryId() {
+        return mCurrentCategoryId;
+    }
+
+    public int getCurrentCategoryPageSize() {
+        return getCategoryPageSize(mCurrentCategoryId);
+    }
+
+    public int getCategoryPageSize(final int categoryId) {
+        for (final CategoryProperties prop : mShownCategories) {
+            if (prop.mCategoryId == categoryId) {
+                return prop.mPageCount;
+            }
+        }
+        Log.w(TAG, "Invalid category id: " + categoryId);
+        // Should not reach here.
+        return 0;
+    }
+
+    public void setCurrentCategoryId(final int categoryId) {
+        mCurrentCategoryId = categoryId;
+        Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
+    }
+
+    public void setCurrentCategoryPageId(final int id) {
+        mCurrentCategoryPageId = id;
+    }
+
+    public int getCurrentCategoryPageId() {
+        return mCurrentCategoryPageId;
+    }
+
+    public void saveLastTypedCategoryPage() {
+        Settings.writeLastTypedEmojiCategoryPageId(
+                mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
+    }
+
+    public boolean isInRecentTab() {
+        return mCurrentCategoryId == EmojiCategory.ID_RECENTS;
+    }
+
+    public int getTabIdFromCategoryId(final 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(final int categoryId) {
+        final int lastSavedCategoryPageId =
+                Settings.readLastTypedEmojiCategoryPageId(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(EmojiCategory.ID_RECENTS);
+    }
+
+    private int getCategoryPageCount(final int categoryId) {
+        final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
+        return (keyboard.getSortedKeys().size() - 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(final int position) {
+        int sum = 0;
+        for (final 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(final int position) {
+        final Pair<Integer, Integer> categoryAndId =
+                getCategoryIdAndPageIdFromPagePosition(position);
+        if (categoryAndId != null) {
+            return getKeyboard(categoryAndId.first, categoryAndId.second);
+        }
+        return null;
+    }
+
+    private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
+        return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
+    }
+
+    public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
+        synchronized (mCategoryKeyboardMap) {
+            final Long categotyKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id);
+            if (mCategoryKeyboardMap.containsKey(categotyKeyboardMapKey)) {
+                return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
+            }
+
+            if (categoryId == EmojiCategory.ID_RECENTS) {
+                final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
+                        mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+                        mMaxPageKeyCount, categoryId);
+                mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd);
+                return kbd;
+            }
+
+            final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
+            final Key[][] sortedKeys = sortKeysIntoPages(
+                    keyboard.getSortedKeys(), mMaxPageKeyCount);
+            for (int pageId = 0; pageId < sortedKeys.length; ++pageId) {
+                final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
+                        mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+                        mMaxPageKeyCount, categoryId);
+                for (final Key emojiKey : sortedKeys[pageId]) {
+                    if (emojiKey == null) {
+                        break;
+                    }
+                    tempKeyboard.addKeyLast(emojiKey);
+                }
+                mCategoryKeyboardMap.put(
+                        getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard);
+            }
+            return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
+        }
+    }
+
+    public int getTotalPageCountOfAllCategories() {
+        int sum = 0;
+        for (CategoryProperties properties : mShownCategories) {
+            sum += properties.mPageCount;
+        }
+        return sum;
+    }
+
+    private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() {
+        @Override
+        public int compare(final Key lhs, final 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;
+        }
+    };
+
+    private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) {
+        final ArrayList<Key> keys = CollectionUtils.newArrayList(inKeys);
+        Collections.sort(keys, EMOJI_KEY_COMPARATOR);
+        final int pageCount = (keys.size() - 1) / maxPageCount + 1;
+        final Key[][] retval = new Key[pageCount][maxPageCount];
+        for (int i = 0; i < keys.size(); ++i) {
+            retval[i / maxPageCount][i % maxPageCount] = keys.get(i);
+        }
+        return retval;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java
index d57ea5a..78af66b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java
@@ -24,7 +24,8 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
-public class EmojiLayoutParams {
+//TODO: Move this class to com.android.inputmethod.emoji package.
+public final class EmojiLayoutParams {
     private static final int DEFAULT_KEYBOARD_ROWS = 4;
 
     public final int mEmojiPagerHeight;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
index e175a05..2f67d19 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
@@ -33,6 +33,7 @@
  * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard.
  * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support.
  */
+// TODO: Move this class to com.android.inputmethod.emoji package.
 // TODO: Implement key popup preview.
 public final class EmojiPageKeyboardView extends KeyboardView implements
         GestureDetector.OnGestureListener {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiPalettesAdapter.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiPalettesAdapter.java
new file mode 100644
index 0000000..a44d134
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiPalettesAdapter.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.support.v4.view.PagerAdapter;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+// TODO: Move this class to com.android.inputmethod.emoji package.
+public final class EmojiPalettesAdapter extends PagerAdapter {
+    private static final String TAG = EmojiPalettesAdapter.class.getSimpleName();
+    private static final boolean DEBUG_PAGER = false;
+
+    private final EmojiPageKeyboardView.OnKeyEventListener mListener;
+    private final DynamicGridKeyboard mRecentsKeyboard;
+    private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews =
+            CollectionUtils.newSparseArray();
+    private final EmojiCategory mEmojiCategory;
+    private int mActivePosition = 0;
+
+    public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
+            final EmojiPageKeyboardView.OnKeyEventListener listener) {
+        mEmojiCategory = emojiCategory;
+        mListener = listener;
+        mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0);
+    }
+
+    public void flushPendingRecentKeys() {
+        mRecentsKeyboard.flushPendingRecentKeys();
+        final KeyboardView recentKeyboardView =
+                mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
+        if (recentKeyboardView != null) {
+            recentKeyboardView.invalidateAllKeys();
+        }
+    }
+
+    public void addRecentKey(final Key key) {
+        if (mEmojiCategory.isInRecentTab()) {
+            mRecentsKeyboard.addPendingKey(key);
+            return;
+        }
+        mRecentsKeyboard.addKeyFirst(key);
+        final KeyboardView recentKeyboardView =
+                mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
+        if (recentKeyboardView != null) {
+            recentKeyboardView.invalidateAllKeys();
+        }
+    }
+
+    public void onPageScrolled() {
+        // Make sure the delayed key-down event (highlight effect and haptic feedback) will be
+        // canceled.
+        final EmojiPageKeyboardView currentKeyboardView =
+                mActiveKeyboardViews.get(mActivePosition);
+        if (currentKeyboardView != null) {
+            currentKeyboardView.releaseCurrentKey();
+        }
+    }
+
+    @Override
+    public int getCount() {
+        return mEmojiCategory.getTotalPageCountOfAllCategories();
+    }
+
+    @Override
+    public void setPrimaryItem(final ViewGroup container, final int position,
+            final Object object) {
+        if (mActivePosition == position) {
+            return;
+        }
+        final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
+        if (oldKeyboardView != null) {
+            oldKeyboardView.releaseCurrentKey();
+            oldKeyboardView.deallocateMemory();
+        }
+        mActivePosition = position;
+    }
+
+    @Override
+    public Object instantiateItem(final ViewGroup container, final int position) {
+        if (DEBUG_PAGER) {
+            Log.d(TAG, "instantiate item: " + position);
+        }
+        final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
+        if (oldKeyboardView != null) {
+            oldKeyboardView.deallocateMemory();
+            // This may be redundant but wanted to be safer..
+            mActiveKeyboardViews.remove(position);
+        }
+        final Keyboard keyboard =
+                mEmojiCategory.getKeyboardFromPagePosition(position);
+        final LayoutInflater inflater = LayoutInflater.from(container.getContext());
+        final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate(
+                R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
+        keyboardView.setKeyboard(keyboard);
+        keyboardView.setOnKeyEventListener(mListener);
+        container.addView(keyboardView);
+        mActiveKeyboardViews.put(position, keyboardView);
+        return keyboardView;
+    }
+
+    @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) {
+        if (DEBUG_PAGER) {
+            Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
+        }
+        final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position);
+        if (keyboardView != null) {
+            keyboardView.deallocateMemory();
+            mActiveKeyboardViews.remove(position);
+        }
+        if (object instanceof View) {
+            container.removeView((View)object);
+        } else {
+            Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsReorderingMyanmar.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsReorderingMyanmar.java
index b3f2819..bb36a2a 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTestsReorderingMyanmar.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsReorderingMyanmar.java
@@ -220,7 +220,7 @@
 
     public void testMyanmarReordering() {
         int testNumber = 0;
-        changeLanguage("mm_MY");
+        changeLanguage("my_MM", "CombiningRules=MyanmarReordering");
         for (final Pair[] test : TESTS) {
             // Small trick to reset LatinIME : setText("") and send updateSelection with values
             // LatinIME has never seen, and cursor pos 0,0.
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 260e534..09c5320 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -299,11 +299,15 @@
     }
 
     protected void changeLanguage(final String locale) {
-        changeLanguageWithoutWait(locale);
+        changeLanguage(locale, null);
+    }
+
+    protected void changeLanguage(final String locale, final String combiningSpec) {
+        changeLanguageWithoutWait(locale, combiningSpec);
         waitForDictionariesToBeLoaded();
     }
 
-    protected void changeLanguageWithoutWait(final String locale) {
+    protected void changeLanguageWithoutWait(final String locale, final String combiningSpec) {
         mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale);
         // TODO: this is forcing a QWERTY keyboard for all locales, which is wrong.
         // It's still better than using whatever keyboard is the current one, but we
@@ -314,7 +318,8 @@
                 "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
                 + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
                 + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
-                + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+                + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE
+                + null == combiningSpec ? "" : ("," + combiningSpec);
         final InputMethodSubtype subtype = InputMethodSubtypeCompatUtils.newInputMethodSubtype(
                 R.string.subtype_no_language_qwerty,
                 R.drawable.ic_ime_switcher_dark,
@@ -325,7 +330,7 @@
                 false /* overridesImplicitlyEnabledSubtype */,
                 0 /* id */);
         SubtypeSwitcher.getInstance().forceSubtype(subtype);
-        mLatinIME.loadKeyboard();
+        mLatinIME.onCurrentInputMethodSubtypeChanged(subtype);
         runMessages();
         mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
         mLatinIME.clearPersonalizedDictionariesForTest();
diff --git a/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java b/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java
index db14b83..0a29d83 100644
--- a/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java
+++ b/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java
@@ -33,7 +33,8 @@
         final int codePointSetSize = 30;
         final int[] codePointSet = CodePointUtils.LATIN_ALPHABETS_LOWER;
         for (int i = 0; i < switchCount; ++i) {
-            changeLanguageWithoutWait(locales[random.nextInt(locales.length)]);
+            changeLanguageWithoutWait(locales[random.nextInt(locales.length)],
+                    null /* combiningSpec */);
             final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration);
             for (int j = 0; j < wordCount; ++j) {
                 final String word = CodePointUtils.generateWord(random, codePointSet);
@@ -50,7 +51,8 @@
         final int codePointSetSize = 30;
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
         for (int i = 0; i < switchCount; ++i) {
-            changeLanguageWithoutWait(locales[random.nextInt(locales.length)]);
+            changeLanguageWithoutWait(locales[random.nextInt(locales.length)],
+                    null /* combiningSpec */);
             final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration);
             for (int j = 0; j < wordCount; ++j) {
                 final String word = CodePointUtils.generateWord(random, codePointSet);