1 /*
2  * Copyright (C) 2014 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.emoji;
18 
19 import android.content.SharedPreferences;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.util.Log;
25 import android.util.Pair;
26 
27 import com.android.inputmethod.compat.BuildCompatUtils;
28 import com.android.inputmethod.keyboard.Key;
29 import com.android.inputmethod.keyboard.Keyboard;
30 import com.android.inputmethod.keyboard.KeyboardId;
31 import com.android.inputmethod.keyboard.KeyboardLayoutSet;
32 import com.android.inputmethod.latin.Constants;
33 import com.android.inputmethod.latin.R;
34 import com.android.inputmethod.latin.settings.Settings;
35 
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.concurrent.ConcurrentHashMap;
42 
43 final class EmojiCategory {
44     private final String TAG = EmojiCategory.class.getSimpleName();
45 
46     private static final int ID_UNSPECIFIED = -1;
47     public static final int ID_RECENTS = 0;
48     private static final int ID_PEOPLE = 1;
49     private static final int ID_OBJECTS = 2;
50     private static final int ID_NATURE = 3;
51     private static final int ID_PLACES = 4;
52     private static final int ID_SYMBOLS = 5;
53     private static final int ID_EMOTICONS = 6;
54 
55     public final class CategoryProperties {
56         public final int mCategoryId;
57         public final int mPageCount;
CategoryProperties(final int categoryId, final int pageCount)58         public CategoryProperties(final int categoryId, final int pageCount) {
59             mCategoryId = categoryId;
60             mPageCount = pageCount;
61         }
62     }
63 
64     private static final String[] sCategoryName = {
65             "recents",
66             "people",
67             "objects",
68             "nature",
69             "places",
70             "symbols",
71             "emoticons" };
72 
73     private static final int[] sCategoryTabIconAttr = {
74             R.styleable.EmojiPalettesView_iconEmojiRecentsTab,
75             R.styleable.EmojiPalettesView_iconEmojiCategory1Tab,
76             R.styleable.EmojiPalettesView_iconEmojiCategory2Tab,
77             R.styleable.EmojiPalettesView_iconEmojiCategory3Tab,
78             R.styleable.EmojiPalettesView_iconEmojiCategory4Tab,
79             R.styleable.EmojiPalettesView_iconEmojiCategory5Tab,
80             R.styleable.EmojiPalettesView_iconEmojiCategory6Tab };
81 
82     private static final int[] sAccessibilityDescriptionResourceIdsForCategories = {
83             R.string.spoken_descrption_emoji_category_recents,
84             R.string.spoken_descrption_emoji_category_people,
85             R.string.spoken_descrption_emoji_category_objects,
86             R.string.spoken_descrption_emoji_category_nature,
87             R.string.spoken_descrption_emoji_category_places,
88             R.string.spoken_descrption_emoji_category_symbols,
89             R.string.spoken_descrption_emoji_category_emoticons };
90 
91     private static final int[] sCategoryElementId = {
92             KeyboardId.ELEMENT_EMOJI_RECENTS,
93             KeyboardId.ELEMENT_EMOJI_CATEGORY1,
94             KeyboardId.ELEMENT_EMOJI_CATEGORY2,
95             KeyboardId.ELEMENT_EMOJI_CATEGORY3,
96             KeyboardId.ELEMENT_EMOJI_CATEGORY4,
97             KeyboardId.ELEMENT_EMOJI_CATEGORY5,
98             KeyboardId.ELEMENT_EMOJI_CATEGORY6 };
99 
100     private final SharedPreferences mPrefs;
101     private final Resources mRes;
102     private final int mMaxPageKeyCount;
103     private final KeyboardLayoutSet mLayoutSet;
104     private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>();
105     private final int[] mCategoryTabIconId = new int[sCategoryName.length];
106     private final ArrayList<CategoryProperties> mShownCategories = new ArrayList<>();
107     private final ConcurrentHashMap<Long, DynamicGridKeyboard> mCategoryKeyboardMap =
108             new ConcurrentHashMap<>();
109 
110     private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED;
111     private int mCurrentCategoryPageId = 0;
112 
EmojiCategory(final SharedPreferences prefs, final Resources res, final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr)113     public EmojiCategory(final SharedPreferences prefs, final Resources res,
114             final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) {
115         mPrefs = prefs;
116         mRes = res;
117         mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count);
118         mLayoutSet = layoutSet;
119         for (int i = 0; i < sCategoryName.length; ++i) {
120             mCategoryNameToIdMap.put(sCategoryName[i], i);
121             mCategoryTabIconId[i] = emojiPaletteViewAttr.getResourceId(
122                     sCategoryTabIconAttr[i], 0);
123         }
124         addShownCategoryId(EmojiCategory.ID_RECENTS);
125         if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.KITKAT) {
126             addShownCategoryId(EmojiCategory.ID_PEOPLE);
127             addShownCategoryId(EmojiCategory.ID_OBJECTS);
128             addShownCategoryId(EmojiCategory.ID_NATURE);
129             addShownCategoryId(EmojiCategory.ID_PLACES);
130             mCurrentCategoryId =
131                     Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_PEOPLE);
132         } else {
133             mCurrentCategoryId =
134                     Settings.readLastShownEmojiCategoryId(mPrefs, EmojiCategory.ID_SYMBOLS);
135         }
136         addShownCategoryId(EmojiCategory.ID_SYMBOLS);
137         addShownCategoryId(EmojiCategory.ID_EMOTICONS);
138         getKeyboard(EmojiCategory.ID_RECENTS, 0 /* cagetoryPageId */)
139                 .loadRecentKeys(mCategoryKeyboardMap.values());
140     }
141 
addShownCategoryId(final int categoryId)142     private void addShownCategoryId(final int categoryId) {
143         // Load a keyboard of categoryId
144         getKeyboard(categoryId, 0 /* cagetoryPageId */);
145         final CategoryProperties properties =
146                 new CategoryProperties(categoryId, getCategoryPageCount(categoryId));
147         mShownCategories.add(properties);
148     }
149 
getCategoryName(final int categoryId, final int categoryPageId)150     public String getCategoryName(final int categoryId, final int categoryPageId) {
151         return sCategoryName[categoryId] + "-" + categoryPageId;
152     }
153 
getCategoryId(final String name)154     public int getCategoryId(final String name) {
155         final String[] strings = name.split("-");
156         return mCategoryNameToIdMap.get(strings[0]);
157     }
158 
getCategoryTabIcon(final int categoryId)159     public int getCategoryTabIcon(final int categoryId) {
160         return mCategoryTabIconId[categoryId];
161     }
162 
getAccessibilityDescription(final int categoryId)163     public String getAccessibilityDescription(final int categoryId) {
164         return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]);
165     }
166 
getShownCategories()167     public ArrayList<CategoryProperties> getShownCategories() {
168         return mShownCategories;
169     }
170 
getCurrentCategoryId()171     public int getCurrentCategoryId() {
172         return mCurrentCategoryId;
173     }
174 
getCurrentCategoryPageSize()175     public int getCurrentCategoryPageSize() {
176         return getCategoryPageSize(mCurrentCategoryId);
177     }
178 
getCategoryPageSize(final int categoryId)179     public int getCategoryPageSize(final int categoryId) {
180         for (final CategoryProperties prop : mShownCategories) {
181             if (prop.mCategoryId == categoryId) {
182                 return prop.mPageCount;
183             }
184         }
185         Log.w(TAG, "Invalid category id: " + categoryId);
186         // Should not reach here.
187         return 0;
188     }
189 
setCurrentCategoryId(final int categoryId)190     public void setCurrentCategoryId(final int categoryId) {
191         mCurrentCategoryId = categoryId;
192         Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
193     }
194 
setCurrentCategoryPageId(final int id)195     public void setCurrentCategoryPageId(final int id) {
196         mCurrentCategoryPageId = id;
197     }
198 
getCurrentCategoryPageId()199     public int getCurrentCategoryPageId() {
200         return mCurrentCategoryPageId;
201     }
202 
saveLastTypedCategoryPage()203     public void saveLastTypedCategoryPage() {
204         Settings.writeLastTypedEmojiCategoryPageId(
205                 mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
206     }
207 
isInRecentTab()208     public boolean isInRecentTab() {
209         return mCurrentCategoryId == EmojiCategory.ID_RECENTS;
210     }
211 
getTabIdFromCategoryId(final int categoryId)212     public int getTabIdFromCategoryId(final int categoryId) {
213         for (int i = 0; i < mShownCategories.size(); ++i) {
214             if (mShownCategories.get(i).mCategoryId == categoryId) {
215                 return i;
216             }
217         }
218         Log.w(TAG, "categoryId not found: " + categoryId);
219         return 0;
220     }
221 
222     // Returns the view pager's page position for the categoryId
getPageIdFromCategoryId(final int categoryId)223     public int getPageIdFromCategoryId(final int categoryId) {
224         final int lastSavedCategoryPageId =
225                 Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId);
226         int sum = 0;
227         for (int i = 0; i < mShownCategories.size(); ++i) {
228             final CategoryProperties props = mShownCategories.get(i);
229             if (props.mCategoryId == categoryId) {
230                 return sum + lastSavedCategoryPageId;
231             }
232             sum += props.mPageCount;
233         }
234         Log.w(TAG, "categoryId not found: " + categoryId);
235         return 0;
236     }
237 
getRecentTabId()238     public int getRecentTabId() {
239         return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
240     }
241 
getCategoryPageCount(final int categoryId)242     private int getCategoryPageCount(final int categoryId) {
243         final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
244         return (keyboard.getSortedKeys().size() - 1) / mMaxPageKeyCount + 1;
245     }
246 
247     // Returns a pair of the category id and the category page id from the view pager's page
248     // position. The category page id is numbered in each category. And the view page position
249     // is the position of the current shown page in the view pager which contains all pages of
250     // all categories.
getCategoryIdAndPageIdFromPagePosition(final int position)251     public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) {
252         int sum = 0;
253         for (final CategoryProperties properties : mShownCategories) {
254             final int temp = sum;
255             sum += properties.mPageCount;
256             if (sum > position) {
257                 return new Pair<>(properties.mCategoryId, position - temp);
258             }
259         }
260         return null;
261     }
262 
263     // Returns a keyboard from the view pager's page position.
getKeyboardFromPagePosition(final int position)264     public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) {
265         final Pair<Integer, Integer> categoryAndId =
266                 getCategoryIdAndPageIdFromPagePosition(position);
267         if (categoryAndId != null) {
268             return getKeyboard(categoryAndId.first, categoryAndId.second);
269         }
270         return null;
271     }
272 
getCategoryKeyboardMapKey(final int categoryId, final int id)273     private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
274         return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
275     }
276 
getKeyboard(final int categoryId, final int id)277     public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
278         synchronized (mCategoryKeyboardMap) {
279             final Long categotyKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id);
280             if (mCategoryKeyboardMap.containsKey(categotyKeyboardMapKey)) {
281                 return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
282             }
283 
284             if (categoryId == EmojiCategory.ID_RECENTS) {
285                 final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
286                         mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
287                         mMaxPageKeyCount, categoryId);
288                 mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd);
289                 return kbd;
290             }
291 
292             final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
293             final Key[][] sortedKeys = sortKeysIntoPages(
294                     keyboard.getSortedKeys(), mMaxPageKeyCount);
295             for (int pageId = 0; pageId < sortedKeys.length; ++pageId) {
296                 final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
297                         mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
298                         mMaxPageKeyCount, categoryId);
299                 for (final Key emojiKey : sortedKeys[pageId]) {
300                     if (emojiKey == null) {
301                         break;
302                     }
303                     tempKeyboard.addKeyLast(emojiKey);
304                 }
305                 mCategoryKeyboardMap.put(
306                         getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard);
307             }
308             return mCategoryKeyboardMap.get(categotyKeyboardMapKey);
309         }
310     }
311 
getTotalPageCountOfAllCategories()312     public int getTotalPageCountOfAllCategories() {
313         int sum = 0;
314         for (CategoryProperties properties : mShownCategories) {
315             sum += properties.mPageCount;
316         }
317         return sum;
318     }
319 
320     private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() {
321         @Override
322         public int compare(final Key lhs, final Key rhs) {
323             final Rect lHitBox = lhs.getHitBox();
324             final Rect rHitBox = rhs.getHitBox();
325             if (lHitBox.top < rHitBox.top) {
326                 return -1;
327             } else if (lHitBox.top > rHitBox.top) {
328                 return 1;
329             }
330             if (lHitBox.left < rHitBox.left) {
331                 return -1;
332             } else if (lHitBox.left > rHitBox.left) {
333                 return 1;
334             }
335             if (lhs.getCode() == rhs.getCode()) {
336                 return 0;
337             }
338             return lhs.getCode() < rhs.getCode() ? -1 : 1;
339         }
340     };
341 
sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount)342     private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) {
343         final ArrayList<Key> keys = new ArrayList<>(inKeys);
344         Collections.sort(keys, EMOJI_KEY_COMPARATOR);
345         final int pageCount = (keys.size() - 1) / maxPageCount + 1;
346         final Key[][] retval = new Key[pageCount][maxPageCount];
347         for (int i = 0; i < keys.size(); ++i) {
348             retval[i / maxPageCount][i % maxPageCount] = keys.get(i);
349         }
350         return retval;
351     }
352 }
353