Home | History | Annotate | Download | only in keyboard
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.keyboard;
     18 
     19 import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
     20 
     21 import android.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.content.res.ColorStateList;
     24 import android.content.res.Resources;
     25 import android.content.res.TypedArray;
     26 import android.graphics.Rect;
     27 import android.os.Build;
     28 import android.preference.PreferenceManager;
     29 import android.support.v4.view.PagerAdapter;
     30 import android.support.v4.view.ViewPager;
     31 import android.text.format.DateUtils;
     32 import android.util.AttributeSet;
     33 import android.util.Log;
     34 import android.util.Pair;
     35 import android.util.SparseArray;
     36 import android.view.LayoutInflater;
     37 import android.view.MotionEvent;
     38 import android.view.View;
     39 import android.view.ViewGroup;
     40 import android.widget.ImageView;
     41 import android.widget.LinearLayout;
     42 import android.widget.TabHost;
     43 import android.widget.TabHost.OnTabChangeListener;
     44 import android.widget.TextView;
     45 
     46 import com.android.inputmethod.keyboard.internal.DynamicGridKeyboard;
     47 import com.android.inputmethod.keyboard.internal.ScrollKeyboardView;
     48 import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier;
     49 import com.android.inputmethod.latin.Constants;
     50 import com.android.inputmethod.latin.R;
     51 import com.android.inputmethod.latin.SubtypeSwitcher;
     52 import com.android.inputmethod.latin.settings.Settings;
     53 import com.android.inputmethod.latin.utils.CollectionUtils;
     54 import com.android.inputmethod.latin.utils.ResourceUtils;
     55 
     56 import java.util.ArrayList;
     57 import java.util.Arrays;
     58 import java.util.Comparator;
     59 import java.util.HashMap;
     60 import java.util.concurrent.ConcurrentHashMap;
     61 
     62 /**
     63  * View class to implement Emoji palettes.
     64  * The Emoji keyboard consists of group of views {@link R.layout#emoji_palettes_view}.
     65  * <ol>
     66  * <li> Emoji category tabs.
     67  * <li> Delete button.
     68  * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
     69  * <li> Back to main keyboard button and enter button.
     70  * </ol>
     71  * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
     72  */
     73 public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
     74         ViewPager.OnPageChangeListener, View.OnClickListener,
     75         ScrollKeyboardView.OnKeyClickListener {
     76     private static final String TAG = EmojiPalettesView.class.getSimpleName();
     77     private static final boolean DEBUG_PAGER = false;
     78     private final int mKeyBackgroundId;
     79     private final int mEmojiFunctionalKeyBackgroundId;
     80     private final KeyboardLayoutSet mLayoutSet;
     81     private final ColorStateList mTabLabelColor;
     82     private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
     83     private EmojiPalettesAdapter mEmojiPalettesAdapter;
     84 
     85     private TabHost mTabHost;
     86     private ViewPager mEmojiPager;
     87     private int mCurrentPagerPosition = 0;
     88     private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
     89 
     90     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
     91 
     92     private static final int CATEGORY_ID_UNSPECIFIED = -1;
     93     public static final int CATEGORY_ID_RECENTS = 0;
     94     public static final int CATEGORY_ID_PEOPLE = 1;
     95     public static final int CATEGORY_ID_OBJECTS = 2;
     96     public static final int CATEGORY_ID_NATURE = 3;
     97     public static final int CATEGORY_ID_PLACES = 4;
     98     public static final int CATEGORY_ID_SYMBOLS = 5;
     99     public static final int CATEGORY_ID_EMOTICONS = 6;
    100 
    101     private static class CategoryProperties {
    102         public int mCategoryId;
    103         public int mPageCount;
    104         public CategoryProperties(final int categoryId, final int pageCount) {
    105             mCategoryId = categoryId;
    106             mPageCount = pageCount;
    107         }
    108     }
    109 
    110     private static class EmojiCategory {
    111         private static final String[] sCategoryName = {
    112                 "recents",
    113                 "people",
    114                 "objects",
    115                 "nature",
    116                 "places",
    117                 "symbols",
    118                 "emoticons" };
    119         private static final int[] sCategoryIcon = new int[] {
    120                 R.drawable.ic_emoji_recent_light,
    121                 R.drawable.ic_emoji_people_light,
    122                 R.drawable.ic_emoji_objects_light,
    123                 R.drawable.ic_emoji_nature_light,
    124                 R.drawable.ic_emoji_places_light,
    125                 R.drawable.ic_emoji_symbols_light,
    126                 0 };
    127         private static final String[] sCategoryLabel =
    128                 { null, null, null, null, null, null, ":-)" };
    129         private static final int[] sCategoryElementId = {
    130                 KeyboardId.ELEMENT_EMOJI_RECENTS,
    131                 KeyboardId.ELEMENT_EMOJI_CATEGORY1,
    132                 KeyboardId.ELEMENT_EMOJI_CATEGORY2,
    133                 KeyboardId.ELEMENT_EMOJI_CATEGORY3,
    134                 KeyboardId.ELEMENT_EMOJI_CATEGORY4,
    135                 KeyboardId.ELEMENT_EMOJI_CATEGORY5,
    136                 KeyboardId.ELEMENT_EMOJI_CATEGORY6 };
    137         private final SharedPreferences mPrefs;
    138         private final int mMaxPageKeyCount;
    139         private final KeyboardLayoutSet mLayoutSet;
    140         private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap();
    141         private final ArrayList<CategoryProperties> mShownCategories =
    142                 CollectionUtils.newArrayList();
    143         private final ConcurrentHashMap<Long, DynamicGridKeyboard>
    144                 mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>();
    145 
    146         private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED;
    147         private int mCurrentCategoryPageId = 0;
    148 
    149         public EmojiCategory(final SharedPreferences prefs, final Resources res,
    150                 final KeyboardLayoutSet layoutSet) {
    151             mPrefs = prefs;
    152             mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count);
    153             mLayoutSet = layoutSet;
    154             for (int i = 0; i < sCategoryName.length; ++i) {
    155                 mCategoryNameToIdMap.put(sCategoryName[i], i);
    156             }
    157             addShownCategoryId(CATEGORY_ID_RECENTS);
    158             if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
    159                     || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie")
    160                     || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) {
    161                 addShownCategoryId(CATEGORY_ID_PEOPLE);
    162                 addShownCategoryId(CATEGORY_ID_OBJECTS);
    163                 addShownCategoryId(CATEGORY_ID_NATURE);
    164                 addShownCategoryId(CATEGORY_ID_PLACES);
    165                 mCurrentCategoryId =
    166                         Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_PEOPLE);
    167             } else {
    168                 mCurrentCategoryId =
    169                         Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_SYMBOLS);
    170             }
    171             addShownCategoryId(CATEGORY_ID_SYMBOLS);
    172             addShownCategoryId(CATEGORY_ID_EMOTICONS);
    173             getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */)
    174                     .loadRecentKeys(mCategoryKeyboardMap.values());
    175         }
    176 
    177         private void addShownCategoryId(int categoryId) {
    178             // Load a keyboard of categoryId
    179             getKeyboard(categoryId, 0 /* cagetoryPageId */);
    180             final CategoryProperties properties =
    181                     new CategoryProperties(categoryId, getCategoryPageCount(categoryId));
    182             mShownCategories.add(properties);
    183         }
    184 
    185         public String getCategoryName(int categoryId, int categoryPageId) {
    186             return sCategoryName[categoryId] + "-" + categoryPageId;
    187         }
    188 
    189         public int getCategoryId(String name) {
    190             final String[] strings = name.split("-");
    191             return mCategoryNameToIdMap.get(strings[0]);
    192         }
    193 
    194         public int getCategoryIcon(int categoryId) {
    195             return sCategoryIcon[categoryId];
    196         }
    197 
    198         public String getCategoryLabel(int categoryId) {
    199             return sCategoryLabel[categoryId];
    200         }
    201 
    202         public ArrayList<CategoryProperties> getShownCategories() {
    203             return mShownCategories;
    204         }
    205 
    206         public int getCurrentCategoryId() {
    207             return mCurrentCategoryId;
    208         }
    209 
    210         public int getCurrentCategoryPageSize() {
    211             return getCategoryPageSize(mCurrentCategoryId);
    212         }
    213 
    214         public int getCategoryPageSize(int categoryId) {
    215             for (final CategoryProperties prop : mShownCategories) {
    216                 if (prop.mCategoryId == categoryId) {
    217                     return prop.mPageCount;
    218                 }
    219             }
    220             Log.w(TAG, "Invalid category id: " + categoryId);
    221             // Should not reach here.
    222             return 0;
    223         }
    224 
    225         public void setCurrentCategoryId(int categoryId) {
    226             mCurrentCategoryId = categoryId;
    227             Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
    228         }
    229 
    230         public void setCurrentCategoryPageId(int id) {
    231             mCurrentCategoryPageId = id;
    232         }
    233 
    234         public int getCurrentCategoryPageId() {
    235             return mCurrentCategoryPageId;
    236         }
    237 
    238         public void saveLastTypedCategoryPage() {
    239             Settings.writeLastTypedEmojiCategoryPageId(
    240                     mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
    241         }
    242 
    243         public boolean isInRecentTab() {
    244             return mCurrentCategoryId == CATEGORY_ID_RECENTS;
    245         }
    246 
    247         public int getTabIdFromCategoryId(int categoryId) {
    248             for (int i = 0; i < mShownCategories.size(); ++i) {
    249                 if (mShownCategories.get(i).mCategoryId == categoryId) {
    250                     return i;
    251                 }
    252             }
    253             Log.w(TAG, "categoryId not found: " + categoryId);
    254             return 0;
    255         }
    256 
    257         // Returns the view pager's page position for the categoryId
    258         public int getPageIdFromCategoryId(int categoryId) {
    259             final int lastSavedCategoryPageId =
    260                     Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId);
    261             int sum = 0;
    262             for (int i = 0; i < mShownCategories.size(); ++i) {
    263                 final CategoryProperties props = mShownCategories.get(i);
    264                 if (props.mCategoryId == categoryId) {
    265                     return sum + lastSavedCategoryPageId;
    266                 }
    267                 sum += props.mPageCount;
    268             }
    269             Log.w(TAG, "categoryId not found: " + categoryId);
    270             return 0;
    271         }
    272 
    273         public int getRecentTabId() {
    274             return getTabIdFromCategoryId(CATEGORY_ID_RECENTS);
    275         }
    276 
    277         private int getCategoryPageCount(int categoryId) {
    278             final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
    279             return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1;
    280         }
    281 
    282         // Returns a pair of the category id and the category page id from the view pager's page
    283         // position. The category page id is numbered in each category. And the view page position
    284         // is the position of the current shown page in the view pager which contains all pages of
    285         // all categories.
    286         public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) {
    287             int sum = 0;
    288             for (CategoryProperties properties : mShownCategories) {
    289                 final int temp = sum;
    290                 sum += properties.mPageCount;
    291                 if (sum > position) {
    292                     return new Pair<Integer, Integer>(properties.mCategoryId, position - temp);
    293                 }
    294             }
    295             return null;
    296         }
    297 
    298         // Returns a keyboard from the view pager's page position.
    299         public DynamicGridKeyboard getKeyboardFromPagePosition(int position) {
    300             final Pair<Integer, Integer> categoryAndId =
    301                     getCategoryIdAndPageIdFromPagePosition(position);
    302             if (categoryAndId != null) {
    303                 return getKeyboard(categoryAndId.first, categoryAndId.second);
    304             }
    305             return null;
    306         }
    307 
    308         public DynamicGridKeyboard getKeyboard(int categoryId, int id) {
    309             synchronized(mCategoryKeyboardMap) {
    310                 final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
    311                 final DynamicGridKeyboard kbd;
    312                 if (!mCategoryKeyboardMap.containsKey(key)) {
    313                     if (categoryId != CATEGORY_ID_RECENTS) {
    314                         final Keyboard keyboard =
    315                                 mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
    316                         final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount);
    317                         for (int i = 0; i < sortedKeys.length; ++i) {
    318                             final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs,
    319                                     mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
    320                                     mMaxPageKeyCount, categoryId, i /* categoryPageId */);
    321                             for (Key emojiKey : sortedKeys[i]) {
    322                                 if (emojiKey == null) {
    323                                     break;
    324                                 }
    325                                 tempKbd.addKeyLast(emojiKey);
    326                             }
    327                             mCategoryKeyboardMap.put((((long) categoryId)
    328                                     << Constants.MAX_INT_BIT_COUNT) | i, tempKbd);
    329                         }
    330                         kbd = mCategoryKeyboardMap.get(key);
    331                     } else {
    332                         kbd = new DynamicGridKeyboard(mPrefs,
    333                                 mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
    334                                 mMaxPageKeyCount, categoryId, 0 /* categoryPageId */);
    335                         mCategoryKeyboardMap.put(key, kbd);
    336                     }
    337                 } else {
    338                     kbd = mCategoryKeyboardMap.get(key);
    339                 }
    340                 return kbd;
    341             }
    342         }
    343 
    344         public int getTotalPageCountOfAllCategories() {
    345             int sum = 0;
    346             for (CategoryProperties properties : mShownCategories) {
    347                 sum += properties.mPageCount;
    348             }
    349             return sum;
    350         }
    351 
    352         private Key[][] sortKeys(Key[] inKeys, int maxPageCount) {
    353             Key[] keys = Arrays.copyOf(inKeys, inKeys.length);
    354             Arrays.sort(keys, 0, keys.length, new Comparator<Key>() {
    355                 @Override
    356                 public int compare(Key lhs, Key rhs) {
    357                     final Rect lHitBox = lhs.getHitBox();
    358                     final Rect rHitBox = rhs.getHitBox();
    359                     if (lHitBox.top < rHitBox.top) {
    360                         return -1;
    361                     } else if (lHitBox.top > rHitBox.top) {
    362                         return 1;
    363                     }
    364                     if (lHitBox.left < rHitBox.left) {
    365                         return -1;
    366                     } else if (lHitBox.left > rHitBox.left) {
    367                         return 1;
    368                     }
    369                     if (lhs.getCode() == rhs.getCode()) {
    370                         return 0;
    371                     }
    372                     return lhs.getCode() < rhs.getCode() ? -1 : 1;
    373                 }
    374             });
    375             final int pageCount = (keys.length - 1) / maxPageCount + 1;
    376             final Key[][] retval = new Key[pageCount][maxPageCount];
    377             for (int i = 0; i < keys.length; ++i) {
    378                 retval[i / maxPageCount][i % maxPageCount] = keys[i];
    379             }
    380             return retval;
    381         }
    382     }
    383 
    384     private final EmojiCategory mEmojiCategory;
    385 
    386     public EmojiPalettesView(final Context context, final AttributeSet attrs) {
    387         this(context, attrs, R.attr.emojiPalettesViewStyle);
    388     }
    389 
    390     public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) {
    391         super(context, attrs, defStyle);
    392         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
    393                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
    394         mKeyBackgroundId = keyboardViewAttr.getResourceId(
    395                 R.styleable.KeyboardView_keyBackground, 0);
    396         mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
    397                 R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0);
    398         keyboardViewAttr.recycle();
    399         final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
    400                 R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
    401         mTabLabelColor = emojiPalettesViewAttr.getColorStateList(
    402                 R.styleable.EmojiPalettesView_emojiTabLabelColor);
    403         emojiPalettesViewAttr.recycle();
    404         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
    405                 context, null /* editorInfo */);
    406         final Resources res = context.getResources();
    407         final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
    408         builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
    409         builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
    410                 emojiLp.mEmojiKeyboardHeight);
    411         builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */);
    412         mLayoutSet = builder.build();
    413         mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
    414                 context.getResources(), builder.build());
    415         mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context);
    416     }
    417 
    418     @Override
    419     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    420         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    421         final Resources res = getContext().getResources();
    422         // The main keyboard expands to the entire this {@link KeyboardView}.
    423         final int width = ResourceUtils.getDefaultKeyboardWidth(res)
    424                 + getPaddingLeft() + getPaddingRight();
    425         final int height = ResourceUtils.getDefaultKeyboardHeight(res)
    426                 + res.getDimensionPixelSize(R.dimen.suggestions_strip_height)
    427                 + getPaddingTop() + getPaddingBottom();
    428         setMeasuredDimension(width, height);
    429     }
    430 
    431     private void addTab(final TabHost host, final int categoryId) {
    432         final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
    433         final TabHost.TabSpec tspec = host.newTabSpec(tabId);
    434         tspec.setContent(R.id.emoji_keyboard_dummy);
    435         if (mEmojiCategory.getCategoryIcon(categoryId) != 0) {
    436             final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
    437                     R.layout.emoji_keyboard_tab_icon, null);
    438             iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId));
    439             tspec.setIndicator(iconView);
    440         }
    441         if (mEmojiCategory.getCategoryLabel(categoryId) != null) {
    442             final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate(
    443                     R.layout.emoji_keyboard_tab_label, null);
    444             textView.setText(mEmojiCategory.getCategoryLabel(categoryId));
    445             textView.setTextColor(mTabLabelColor);
    446             tspec.setIndicator(textView);
    447         }
    448         host.addTab(tspec);
    449     }
    450 
    451     @Override
    452     protected void onFinishInflate() {
    453         mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
    454         mTabHost.setup();
    455         for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) {
    456             addTab(mTabHost, properties.mCategoryId);
    457         }
    458         mTabHost.setOnTabChangedListener(this);
    459         mTabHost.getTabWidget().setStripEnabled(true);
    460 
    461         mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mLayoutSet, this);
    462 
    463         mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
    464         mEmojiPager.setAdapter(mEmojiPalettesAdapter);
    465         mEmojiPager.setOnPageChangeListener(this);
    466         mEmojiPager.setOffscreenPageLimit(0);
    467         mEmojiPager.setPersistentDrawingCache(ViewPager.PERSISTENT_NO_CACHE);
    468         final Resources res = getResources();
    469         final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
    470         emojiLp.setPagerProperties(mEmojiPager);
    471 
    472         mEmojiCategoryPageIndicatorView =
    473                 (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view);
    474         emojiLp.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
    475 
    476         setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */);
    477 
    478         final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar);
    479         emojiLp.setActionBarProperties(actionBar);
    480 
    481         final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete);
    482         deleteKey.setTag(Constants.CODE_DELETE);
    483         deleteKey.setOnTouchListener(mDeleteKeyOnTouchListener);
    484         final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet);
    485         alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
    486         alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
    487         alphabetKey.setOnClickListener(this);
    488         final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space);
    489         spaceKey.setBackgroundResource(mKeyBackgroundId);
    490         spaceKey.setTag(Constants.CODE_SPACE);
    491         spaceKey.setOnClickListener(this);
    492         emojiLp.setKeyProperties(spaceKey);
    493         final ImageView alphabetKey2 = (ImageView)findViewById(R.id.emoji_keyboard_alphabet2);
    494         alphabetKey2.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
    495         alphabetKey2.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
    496         alphabetKey2.setOnClickListener(this);
    497     }
    498 
    499     @Override
    500     public void onTabChanged(final String tabId) {
    501         final int categoryId = mEmojiCategory.getCategoryId(tabId);
    502         setCurrentCategoryId(categoryId, false /* force */);
    503         updateEmojiCategoryPageIdView();
    504     }
    505 
    506 
    507     @Override
    508     public void onPageSelected(final int position) {
    509         final Pair<Integer, Integer> newPos =
    510                 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
    511         setCurrentCategoryId(newPos.first /* categoryId */, false /* force */);
    512         mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */);
    513         updateEmojiCategoryPageIdView();
    514         mCurrentPagerPosition = position;
    515     }
    516 
    517     @Override
    518     public void onPageScrollStateChanged(final int state) {
    519         // Ignore this message. Only want the actual page selected.
    520     }
    521 
    522     @Override
    523     public void onPageScrolled(final int position, final float positionOffset,
    524             final int positionOffsetPixels) {
    525         final Pair<Integer, Integer> newPos =
    526                 mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
    527         final int newCategoryId = newPos.first;
    528         final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId);
    529         final int currentCategoryId = mEmojiCategory.getCurrentCategoryId();
    530         final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
    531         final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize();
    532         if (newCategoryId == currentCategoryId) {
    533             mEmojiCategoryPageIndicatorView.setCategoryPageId(
    534                     newCategorySize, newPos.second, positionOffset);
    535         } else if (newCategoryId > currentCategoryId) {
    536             mEmojiCategoryPageIndicatorView.setCategoryPageId(
    537                     currentCategorySize, currentCategoryPageId, positionOffset);
    538         } else if (newCategoryId < currentCategoryId) {
    539             mEmojiCategoryPageIndicatorView.setCategoryPageId(
    540                     currentCategorySize, currentCategoryPageId, positionOffset - 1);
    541         }
    542     }
    543 
    544     @Override
    545     public void onClick(final View v) {
    546         if (v.getTag() instanceof Integer) {
    547             final int code = (Integer)v.getTag();
    548             registerCode(code);
    549             return;
    550         }
    551     }
    552 
    553     private void registerCode(final int code) {
    554         mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */);
    555         mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE);
    556         mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
    557     }
    558 
    559     @Override
    560     public void onKeyClick(final Key key) {
    561         mEmojiPalettesAdapter.addRecentKey(key);
    562         mEmojiCategory.saveLastTypedCategoryPage();
    563         final int code = key.getCode();
    564         if (code == Constants.CODE_OUTPUT_TEXT) {
    565             mKeyboardActionListener.onTextInput(key.getOutputText());
    566             return;
    567         }
    568         registerCode(code);
    569     }
    570 
    571     public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
    572         // TODO:
    573     }
    574 
    575     public void startEmojiPalettes() {
    576         if (DEBUG_PAGER) {
    577             Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition);
    578         }
    579         mEmojiPager.setAdapter(mEmojiPalettesAdapter);
    580         mEmojiPager.setCurrentItem(mCurrentPagerPosition);
    581     }
    582 
    583     public void stopEmojiPalettes() {
    584         if (DEBUG_PAGER) {
    585             Log.d(TAG, "deallocate emoji palettes memory");
    586         }
    587         mEmojiPalettesAdapter.flushPendingRecentKeys();
    588         mEmojiPager.setAdapter(null);
    589     }
    590 
    591     public void setKeyboardActionListener(final KeyboardActionListener listener) {
    592         mKeyboardActionListener = listener;
    593         mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
    594     }
    595 
    596     private void updateEmojiCategoryPageIdView() {
    597         if (mEmojiCategoryPageIndicatorView == null) {
    598             return;
    599         }
    600         mEmojiCategoryPageIndicatorView.setCategoryPageId(
    601                 mEmojiCategory.getCurrentCategoryPageSize(),
    602                 mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
    603     }
    604 
    605     private void setCurrentCategoryId(final int categoryId, final boolean force) {
    606         final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
    607         if (oldCategoryId == categoryId && !force) {
    608             return;
    609         }
    610 
    611         if (oldCategoryId == CATEGORY_ID_RECENTS) {
    612             // Needs to save pending updates for recent keys when we get out of the recents
    613             // category because we don't want to move the recent emojis around while the user
    614             // is in the recents category.
    615             mEmojiPalettesAdapter.flushPendingRecentKeys();
    616         }
    617 
    618         mEmojiCategory.setCurrentCategoryId(categoryId);
    619         final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
    620         final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
    621         if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
    622                 mEmojiPager.getCurrentItem()).first != categoryId) {
    623             mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
    624         }
    625         if (force || mTabHost.getCurrentTab() != newTabId) {
    626             mTabHost.setCurrentTab(newTabId);
    627         }
    628     }
    629 
    630     private static class EmojiPalettesAdapter extends PagerAdapter {
    631         private final ScrollKeyboardView.OnKeyClickListener mListener;
    632         private final DynamicGridKeyboard mRecentsKeyboard;
    633         private final SparseArray<ScrollKeyboardView> mActiveKeyboardViews =
    634                 CollectionUtils.newSparseArray();
    635         private final EmojiCategory mEmojiCategory;
    636         private int mActivePosition = 0;
    637 
    638         public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
    639                 final KeyboardLayoutSet layoutSet,
    640                 final ScrollKeyboardView.OnKeyClickListener listener) {
    641             mEmojiCategory = emojiCategory;
    642             mListener = listener;
    643             mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0);
    644         }
    645 
    646         public void flushPendingRecentKeys() {
    647             mRecentsKeyboard.flushPendingRecentKeys();
    648             final KeyboardView recentKeyboardView =
    649                     mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
    650             if (recentKeyboardView != null) {
    651                 recentKeyboardView.invalidateAllKeys();
    652             }
    653         }
    654 
    655         public void addRecentKey(final Key key) {
    656             if (mEmojiCategory.isInRecentTab()) {
    657                 mRecentsKeyboard.addPendingKey(key);
    658                 return;
    659             }
    660             mRecentsKeyboard.addKeyFirst(key);
    661             final KeyboardView recentKeyboardView =
    662                     mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
    663             if (recentKeyboardView != null) {
    664                 recentKeyboardView.invalidateAllKeys();
    665             }
    666         }
    667 
    668         @Override
    669         public int getCount() {
    670             return mEmojiCategory.getTotalPageCountOfAllCategories();
    671         }
    672 
    673         @Override
    674         public void setPrimaryItem(final View container, final int position, final Object object) {
    675             if (mActivePosition == position) {
    676                 return;
    677             }
    678             final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
    679             if (oldKeyboardView != null) {
    680                 oldKeyboardView.releaseCurrentKey();
    681                 oldKeyboardView.deallocateMemory();
    682             }
    683             mActivePosition = position;
    684         }
    685 
    686         @Override
    687         public Object instantiateItem(final ViewGroup container, final int position) {
    688             if (DEBUG_PAGER) {
    689                 Log.d(TAG, "instantiate item: " + position);
    690             }
    691             final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
    692             if (oldKeyboardView != null) {
    693                 oldKeyboardView.deallocateMemory();
    694                 // This may be redundant but wanted to be safer..
    695                 mActiveKeyboardViews.remove(position);
    696             }
    697             final Keyboard keyboard =
    698                     mEmojiCategory.getKeyboardFromPagePosition(position);
    699             final LayoutInflater inflater = LayoutInflater.from(container.getContext());
    700             final View view = inflater.inflate(
    701                     R.layout.emoji_keyboard_page, container, false /* attachToRoot */);
    702             final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById(
    703                     R.id.emoji_keyboard_page);
    704             keyboardView.setKeyboard(keyboard);
    705             keyboardView.setOnKeyClickListener(mListener);
    706             final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById(
    707                     R.id.emoji_keyboard_scroller);
    708             keyboardView.setScrollView(scrollView);
    709             container.addView(view);
    710             mActiveKeyboardViews.put(position, keyboardView);
    711             return view;
    712         }
    713 
    714         @Override
    715         public boolean isViewFromObject(final View view, final Object object) {
    716             return view == object;
    717         }
    718 
    719         @Override
    720         public void destroyItem(final ViewGroup container, final int position,
    721                 final Object object) {
    722             if (DEBUG_PAGER) {
    723                 Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
    724             }
    725             final ScrollKeyboardView keyboardView = mActiveKeyboardViews.get(position);
    726             if (keyboardView != null) {
    727                 keyboardView.deallocateMemory();
    728                 mActiveKeyboardViews.remove(position);
    729             }
    730             if (object instanceof View) {
    731                 container.removeView((View)object);
    732             } else {
    733                 Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
    734             }
    735         }
    736     }
    737 
    738     // TODO: Do the same things done in PointerTracker
    739     private static class DeleteKeyOnTouchListener implements OnTouchListener {
    740         private static final long MAX_REPEAT_COUNT_TIME = 30 * DateUtils.SECOND_IN_MILLIS;
    741         private final int mDeleteKeyPressedBackgroundColor;
    742         private final long mKeyRepeatStartTimeout;
    743         private final long mKeyRepeatInterval;
    744 
    745         public DeleteKeyOnTouchListener(Context context) {
    746             final Resources res = context.getResources();
    747             mDeleteKeyPressedBackgroundColor =
    748                     res.getColor(R.color.emoji_key_pressed_background_color);
    749             mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout);
    750             mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
    751         }
    752 
    753         private KeyboardActionListener mKeyboardActionListener =
    754                 KeyboardActionListener.EMPTY_LISTENER;
    755         private DummyRepeatKeyRepeatTimer mTimer;
    756 
    757         private synchronized void startRepeat() {
    758             if (mTimer != null) {
    759                 abortRepeat();
    760             }
    761             mTimer = new DummyRepeatKeyRepeatTimer();
    762             mTimer.start();
    763         }
    764 
    765         private synchronized void abortRepeat() {
    766             mTimer.abort();
    767             mTimer = null;
    768         }
    769 
    770         // TODO: Remove
    771         // This function is mimicking the repeat code in PointerTracker.
    772         // Specifically referring to PointerTracker#startRepeatKey and PointerTracker#onKeyRepeat.
    773         private class DummyRepeatKeyRepeatTimer extends Thread {
    774             public boolean mAborted = false;
    775 
    776             @Override
    777             public void run() {
    778                 int repeatCount = 1;
    779                 int timeCount = 0;
    780                 while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) {
    781                     if (timeCount > mKeyRepeatStartTimeout) {
    782                         pressDelete(repeatCount);
    783                     }
    784                     timeCount += mKeyRepeatInterval;
    785                     ++repeatCount;
    786                     try {
    787                         Thread.sleep(mKeyRepeatInterval);
    788                     } catch (InterruptedException e) {
    789                     }
    790                 }
    791             }
    792 
    793             public void abort() {
    794                 mAborted = true;
    795             }
    796         }
    797 
    798         public void pressDelete(int repeatCount) {
    799             mKeyboardActionListener.onPressKey(
    800                     Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */);
    801             mKeyboardActionListener.onCodeInput(
    802                     Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE);
    803             mKeyboardActionListener.onReleaseKey(
    804                     Constants.CODE_DELETE, false /* withSliding */);
    805         }
    806 
    807         public void setKeyboardActionListener(KeyboardActionListener listener) {
    808             mKeyboardActionListener = listener;
    809         }
    810 
    811         @Override
    812         public boolean onTouch(View v, MotionEvent event) {
    813             switch(event.getAction()) {
    814                 case MotionEvent.ACTION_DOWN:
    815                     v.setBackgroundColor(mDeleteKeyPressedBackgroundColor);
    816                     pressDelete(0 /* repeatCount */);
    817                     startRepeat();
    818                     return true;
    819                 case MotionEvent.ACTION_UP:
    820                     v.setBackgroundColor(0);
    821                     abortRepeat();
    822                     return true;
    823             }
    824             return false;
    825         }
    826     }
    827 }
    828