• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
CategoryProperties(final int categoryId, final int pageCount)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 
EmojiCategory(final SharedPreferences prefs, final Resources res, final KeyboardLayoutSet layoutSet)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 
addShownCategoryId(int categoryId)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 
getCategoryName(int categoryId, int categoryPageId)185         public String getCategoryName(int categoryId, int categoryPageId) {
186             return sCategoryName[categoryId] + "-" + categoryPageId;
187         }
188 
getCategoryId(String name)189         public int getCategoryId(String name) {
190             final String[] strings = name.split("-");
191             return mCategoryNameToIdMap.get(strings[0]);
192         }
193 
getCategoryIcon(int categoryId)194         public int getCategoryIcon(int categoryId) {
195             return sCategoryIcon[categoryId];
196         }
197 
getCategoryLabel(int categoryId)198         public String getCategoryLabel(int categoryId) {
199             return sCategoryLabel[categoryId];
200         }
201 
getShownCategories()202         public ArrayList<CategoryProperties> getShownCategories() {
203             return mShownCategories;
204         }
205 
getCurrentCategoryId()206         public int getCurrentCategoryId() {
207             return mCurrentCategoryId;
208         }
209 
getCurrentCategoryPageSize()210         public int getCurrentCategoryPageSize() {
211             return getCategoryPageSize(mCurrentCategoryId);
212         }
213 
getCategoryPageSize(int categoryId)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 
setCurrentCategoryId(int categoryId)225         public void setCurrentCategoryId(int categoryId) {
226             mCurrentCategoryId = categoryId;
227             Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
228         }
229 
setCurrentCategoryPageId(int id)230         public void setCurrentCategoryPageId(int id) {
231             mCurrentCategoryPageId = id;
232         }
233 
getCurrentCategoryPageId()234         public int getCurrentCategoryPageId() {
235             return mCurrentCategoryPageId;
236         }
237 
saveLastTypedCategoryPage()238         public void saveLastTypedCategoryPage() {
239             Settings.writeLastTypedEmojiCategoryPageId(
240                     mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
241         }
242 
isInRecentTab()243         public boolean isInRecentTab() {
244             return mCurrentCategoryId == CATEGORY_ID_RECENTS;
245         }
246 
getTabIdFromCategoryId(int categoryId)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
getPageIdFromCategoryId(int 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 
getRecentTabId()273         public int getRecentTabId() {
274             return getTabIdFromCategoryId(CATEGORY_ID_RECENTS);
275         }
276 
getCategoryPageCount(int categoryId)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.
getCategoryIdAndPageIdFromPagePosition(int position)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.
getKeyboardFromPagePosition(int 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 
getKeyboard(int categoryId, int id)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 
getTotalPageCountOfAllCategories()344         public int getTotalPageCountOfAllCategories() {
345             int sum = 0;
346             for (CategoryProperties properties : mShownCategories) {
347                 sum += properties.mPageCount;
348             }
349             return sum;
350         }
351 
sortKeys(Key[] inKeys, int maxPageCount)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 
EmojiPalettesView(final Context context, final AttributeSet attrs)386     public EmojiPalettesView(final Context context, final AttributeSet attrs) {
387         this(context, attrs, R.attr.emojiPalettesViewStyle);
388     }
389 
EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle)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
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)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 
addTab(final TabHost host, final int categoryId)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
onFinishInflate()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
onTabChanged(final String tabId)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
onPageSelected(final int position)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
onPageScrollStateChanged(final int state)518     public void onPageScrollStateChanged(final int state) {
519         // Ignore this message. Only want the actual page selected.
520     }
521 
522     @Override
onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels)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
onClick(final View v)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 
registerCode(final int code)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
onKeyClick(final Key key)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 
setHardwareAcceleratedDrawingEnabled(final boolean enabled)571     public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
572         // TODO:
573     }
574 
startEmojiPalettes()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 
stopEmojiPalettes()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 
setKeyboardActionListener(final KeyboardActionListener listener)591     public void setKeyboardActionListener(final KeyboardActionListener listener) {
592         mKeyboardActionListener = listener;
593         mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
594     }
595 
updateEmojiCategoryPageIdView()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 
setCurrentCategoryId(final int categoryId, final boolean force)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 
EmojiPalettesAdapter(final EmojiCategory emojiCategory, final KeyboardLayoutSet layoutSet, final ScrollKeyboardView.OnKeyClickListener listener)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 
flushPendingRecentKeys()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 
addRecentKey(final Key key)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
getCount()669         public int getCount() {
670             return mEmojiCategory.getTotalPageCountOfAllCategories();
671         }
672 
673         @Override
setPrimaryItem(final View container, final int position, final Object object)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
instantiateItem(final ViewGroup container, final int position)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
isViewFromObject(final View view, final Object object)715         public boolean isViewFromObject(final View view, final Object object) {
716             return view == object;
717         }
718 
719         @Override
destroyItem(final ViewGroup container, final int position, final Object object)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 
DeleteKeyOnTouchListener(Context context)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 
startRepeat()757         private synchronized void startRepeat() {
758             if (mTimer != null) {
759                 abortRepeat();
760             }
761             mTimer = new DummyRepeatKeyRepeatTimer();
762             mTimer.start();
763         }
764 
abortRepeat()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
run()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 
abort()793             public void abort() {
794                 mAborted = true;
795             }
796         }
797 
pressDelete(int repeatCount)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 
setKeyboardActionListener(KeyboardActionListener listener)807         public void setKeyboardActionListener(KeyboardActionListener listener) {
808             mKeyboardActionListener = listener;
809         }
810 
811         @Override
onTouch(View v, MotionEvent event)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