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
17package com.android.inputmethod.keyboard.emoji;
18
19import android.content.SharedPreferences;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Rect;
23import android.os.Build;
24import android.util.Log;
25import android.util.Pair;
26
27import com.android.inputmethod.compat.BuildCompatUtils;
28import com.android.inputmethod.keyboard.Key;
29import com.android.inputmethod.keyboard.Keyboard;
30import com.android.inputmethod.keyboard.KeyboardId;
31import com.android.inputmethod.keyboard.KeyboardLayoutSet;
32import com.android.inputmethod.latin.Constants;
33import com.android.inputmethod.latin.R;
34import com.android.inputmethod.latin.settings.Settings;
35
36import java.util.ArrayList;
37import java.util.Collections;
38import java.util.Comparator;
39import java.util.HashMap;
40import java.util.List;
41import java.util.concurrent.ConcurrentHashMap;
42
43final 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;
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
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
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
150    public String getCategoryName(final int categoryId, final int categoryPageId) {
151        return sCategoryName[categoryId] + "-" + categoryPageId;
152    }
153
154    public int getCategoryId(final String name) {
155        final String[] strings = name.split("-");
156        return mCategoryNameToIdMap.get(strings[0]);
157    }
158
159    public int getCategoryTabIcon(final int categoryId) {
160        return mCategoryTabIconId[categoryId];
161    }
162
163    public String getAccessibilityDescription(final int categoryId) {
164        return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]);
165    }
166
167    public ArrayList<CategoryProperties> getShownCategories() {
168        return mShownCategories;
169    }
170
171    public int getCurrentCategoryId() {
172        return mCurrentCategoryId;
173    }
174
175    public int getCurrentCategoryPageSize() {
176        return getCategoryPageSize(mCurrentCategoryId);
177    }
178
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
190    public void setCurrentCategoryId(final int categoryId) {
191        mCurrentCategoryId = categoryId;
192        Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
193    }
194
195    public void setCurrentCategoryPageId(final int id) {
196        mCurrentCategoryPageId = id;
197    }
198
199    public int getCurrentCategoryPageId() {
200        return mCurrentCategoryPageId;
201    }
202
203    public void saveLastTypedCategoryPage() {
204        Settings.writeLastTypedEmojiCategoryPageId(
205                mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
206    }
207
208    public boolean isInRecentTab() {
209        return mCurrentCategoryId == EmojiCategory.ID_RECENTS;
210    }
211
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
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
238    public int getRecentTabId() {
239        return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
240    }
241
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.
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.
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
273    private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
274        return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id;
275    }
276
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
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
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