1/*
2 * Copyright (C) 2015 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.Paint;
23import android.graphics.Rect;
24import android.os.Build;
25import android.util.Log;
26import android.util.Pair;
27
28import com.android.inputmethod.compat.BuildCompatUtils;
29import com.android.inputmethod.keyboard.Key;
30import com.android.inputmethod.keyboard.Keyboard;
31import com.android.inputmethod.keyboard.KeyboardId;
32import com.android.inputmethod.keyboard.KeyboardLayoutSet;
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    private static final int ID_FLAGS = 7;
55    private static final int ID_EIGHT_SMILEY_PEOPLE = 8;
56    private static final int ID_EIGHT_ANIMALS_NATURE = 9;
57    private static final int ID_EIGHT_FOOD_DRINK = 10;
58    private static final int ID_EIGHT_TRAVEL_PLACES = 11;
59    private static final int ID_EIGHT_ACTIVITY = 12;
60    private static final int ID_EIGHT_OBJECTS = 13;
61    private static final int ID_EIGHT_SYMBOLS = 14;
62    private static final int ID_EIGHT_FLAGS = 15;
63    private static final int ID_EIGHT_SMILEY_PEOPLE_BORING = 16;
64
65    public final class CategoryProperties {
66        public final int mCategoryId;
67        public final int mPageCount;
68        public CategoryProperties(final int categoryId, final int pageCount) {
69            mCategoryId = categoryId;
70            mPageCount = pageCount;
71        }
72    }
73
74    private static final String[] sCategoryName = {
75            "recents",
76            "people",
77            "objects",
78            "nature",
79            "places",
80            "symbols",
81            "emoticons",
82            "flags",
83            "smiley & people",
84            "animals & nature",
85            "food & drink",
86            "travel & places",
87            "activity",
88            "objects2",
89            "symbols2",
90            "flags2",
91            "smiley & people2" };
92
93    private static final int[] sCategoryTabIconAttr = {
94            R.styleable.EmojiPalettesView_iconEmojiRecentsTab,
95            R.styleable.EmojiPalettesView_iconEmojiCategory1Tab,
96            R.styleable.EmojiPalettesView_iconEmojiCategory2Tab,
97            R.styleable.EmojiPalettesView_iconEmojiCategory3Tab,
98            R.styleable.EmojiPalettesView_iconEmojiCategory4Tab,
99            R.styleable.EmojiPalettesView_iconEmojiCategory5Tab,
100            R.styleable.EmojiPalettesView_iconEmojiCategory6Tab,
101            R.styleable.EmojiPalettesView_iconEmojiCategory7Tab,
102            R.styleable.EmojiPalettesView_iconEmojiCategory8Tab,
103            R.styleable.EmojiPalettesView_iconEmojiCategory9Tab,
104            R.styleable.EmojiPalettesView_iconEmojiCategory10Tab,
105            R.styleable.EmojiPalettesView_iconEmojiCategory11Tab,
106            R.styleable.EmojiPalettesView_iconEmojiCategory12Tab,
107            R.styleable.EmojiPalettesView_iconEmojiCategory13Tab,
108            R.styleable.EmojiPalettesView_iconEmojiCategory14Tab,
109            R.styleable.EmojiPalettesView_iconEmojiCategory15Tab,
110            R.styleable.EmojiPalettesView_iconEmojiCategory16Tab };
111
112    private static final int[] sAccessibilityDescriptionResourceIdsForCategories = {
113            R.string.spoken_descrption_emoji_category_recents,
114            R.string.spoken_descrption_emoji_category_people,
115            R.string.spoken_descrption_emoji_category_objects,
116            R.string.spoken_descrption_emoji_category_nature,
117            R.string.spoken_descrption_emoji_category_places,
118            R.string.spoken_descrption_emoji_category_symbols,
119            R.string.spoken_descrption_emoji_category_emoticons,
120            R.string.spoken_descrption_emoji_category_flags,
121            R.string.spoken_descrption_emoji_category_eight_smiley_people,
122            R.string.spoken_descrption_emoji_category_eight_animals_nature,
123            R.string.spoken_descrption_emoji_category_eight_food_drink,
124            R.string.spoken_descrption_emoji_category_eight_travel_places,
125            R.string.spoken_descrption_emoji_category_eight_activity,
126            R.string.spoken_descrption_emoji_category_objects,
127            R.string.spoken_descrption_emoji_category_symbols,
128            R.string.spoken_descrption_emoji_category_flags,
129            R.string.spoken_descrption_emoji_category_eight_smiley_people };
130
131    private static final int[] sCategoryElementId = {
132            KeyboardId.ELEMENT_EMOJI_RECENTS,
133            KeyboardId.ELEMENT_EMOJI_CATEGORY1,
134            KeyboardId.ELEMENT_EMOJI_CATEGORY2,
135            KeyboardId.ELEMENT_EMOJI_CATEGORY3,
136            KeyboardId.ELEMENT_EMOJI_CATEGORY4,
137            KeyboardId.ELEMENT_EMOJI_CATEGORY5,
138            KeyboardId.ELEMENT_EMOJI_CATEGORY6,
139            KeyboardId.ELEMENT_EMOJI_CATEGORY7,
140            KeyboardId.ELEMENT_EMOJI_CATEGORY8,
141            KeyboardId.ELEMENT_EMOJI_CATEGORY9,
142            KeyboardId.ELEMENT_EMOJI_CATEGORY10,
143            KeyboardId.ELEMENT_EMOJI_CATEGORY11,
144            KeyboardId.ELEMENT_EMOJI_CATEGORY12,
145            KeyboardId.ELEMENT_EMOJI_CATEGORY13,
146            KeyboardId.ELEMENT_EMOJI_CATEGORY14,
147            KeyboardId.ELEMENT_EMOJI_CATEGORY15,
148            KeyboardId.ELEMENT_EMOJI_CATEGORY16 };
149
150    private final SharedPreferences mPrefs;
151    private final Resources mRes;
152    private final int mMaxPageKeyCount;
153    private final KeyboardLayoutSet mLayoutSet;
154    private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>();
155    private final int[] mCategoryTabIconId = new int[sCategoryName.length];
156    private final ArrayList<CategoryProperties> mShownCategories = new ArrayList<>();
157    private final ConcurrentHashMap<Long, DynamicGridKeyboard> mCategoryKeyboardMap =
158            new ConcurrentHashMap<>();
159
160    private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED;
161    private int mCurrentCategoryPageId = 0;
162
163    public EmojiCategory(final SharedPreferences prefs, final Resources res,
164            final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) {
165        mPrefs = prefs;
166        mRes = res;
167        mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count);
168        mLayoutSet = layoutSet;
169        for (int i = 0; i < sCategoryName.length; ++i) {
170            mCategoryNameToIdMap.put(sCategoryName[i], i);
171            mCategoryTabIconId[i] = emojiPaletteViewAttr.getResourceId(
172                    sCategoryTabIconAttr[i], 0);
173        }
174
175        int defaultCategoryId = EmojiCategory.ID_SYMBOLS;
176        addShownCategoryId(EmojiCategory.ID_RECENTS);
177        if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.KITKAT) {
178            if (canShowUnicodeEightEmoji()) {
179                defaultCategoryId = EmojiCategory.ID_EIGHT_SMILEY_PEOPLE;
180                addShownCategoryId(EmojiCategory.ID_EIGHT_SMILEY_PEOPLE);
181                addShownCategoryId(EmojiCategory.ID_EIGHT_ANIMALS_NATURE);
182                addShownCategoryId(EmojiCategory.ID_EIGHT_FOOD_DRINK);
183                addShownCategoryId(EmojiCategory.ID_EIGHT_TRAVEL_PLACES);
184                addShownCategoryId(EmojiCategory.ID_EIGHT_ACTIVITY);
185                addShownCategoryId(EmojiCategory.ID_EIGHT_OBJECTS);
186                addShownCategoryId(EmojiCategory.ID_EIGHT_SYMBOLS);
187                addShownCategoryId(EmojiCategory.ID_FLAGS); // Exclude combinations without glyphs.
188            } else {
189                defaultCategoryId = EmojiCategory.ID_PEOPLE;
190                addShownCategoryId(EmojiCategory.ID_PEOPLE);
191                addShownCategoryId(EmojiCategory.ID_OBJECTS);
192                addShownCategoryId(EmojiCategory.ID_NATURE);
193                addShownCategoryId(EmojiCategory.ID_PLACES);
194                addShownCategoryId(EmojiCategory.ID_SYMBOLS);
195                if (canShowFlagEmoji()) {
196                    addShownCategoryId(EmojiCategory.ID_FLAGS);
197                }
198            }
199        } else {
200            addShownCategoryId(EmojiCategory.ID_SYMBOLS);
201        }
202        addShownCategoryId(EmojiCategory.ID_EMOTICONS);
203
204        DynamicGridKeyboard recentsKbd =
205                getKeyboard(EmojiCategory.ID_RECENTS, 0 /* categoryPageId */);
206        recentsKbd.loadRecentKeys(mCategoryKeyboardMap.values());
207
208        mCurrentCategoryId = Settings.readLastShownEmojiCategoryId(mPrefs, defaultCategoryId);
209        Log.i(TAG, "Last Emoji category id is " + mCurrentCategoryId);
210        if (!isShownCategoryId(mCurrentCategoryId)) {
211            Log.i(TAG, "Last emoji category " + mCurrentCategoryId +
212                    " is invalid, starting in " + defaultCategoryId);
213            mCurrentCategoryId = defaultCategoryId;
214        } else if (mCurrentCategoryId == EmojiCategory.ID_RECENTS &&
215                recentsKbd.getSortedKeys().isEmpty()) {
216            Log.i(TAG, "No recent emojis found, starting in category " + defaultCategoryId);
217            mCurrentCategoryId = defaultCategoryId;
218        }
219    }
220
221    private void addShownCategoryId(final int categoryId) {
222        // Load a keyboard of categoryId
223        getKeyboard(categoryId, 0 /* categoryPageId */);
224        final CategoryProperties properties =
225                new CategoryProperties(categoryId, getCategoryPageCount(categoryId));
226        mShownCategories.add(properties);
227    }
228
229    private boolean isShownCategoryId(final int categoryId) {
230        for (final CategoryProperties prop : mShownCategories) {
231            if (prop.mCategoryId == categoryId) {
232                return true;
233            }
234        }
235        return false;
236    }
237
238    public static String getCategoryName(final int categoryId, final int categoryPageId) {
239        return sCategoryName[categoryId] + "-" + categoryPageId;
240    }
241
242    public int getCategoryId(final String name) {
243        final String[] strings = name.split("-");
244        return mCategoryNameToIdMap.get(strings[0]);
245    }
246
247    public int getCategoryTabIcon(final int categoryId) {
248        return mCategoryTabIconId[categoryId];
249    }
250
251    public String getAccessibilityDescription(final int categoryId) {
252        return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]);
253    }
254
255    public ArrayList<CategoryProperties> getShownCategories() {
256        return mShownCategories;
257    }
258
259    public int getCurrentCategoryId() {
260        return mCurrentCategoryId;
261    }
262
263    public int getCurrentCategoryPageSize() {
264        return getCategoryPageSize(mCurrentCategoryId);
265    }
266
267    public int getCategoryPageSize(final int categoryId) {
268        for (final CategoryProperties prop : mShownCategories) {
269            if (prop.mCategoryId == categoryId) {
270                return prop.mPageCount;
271            }
272        }
273        Log.w(TAG, "Invalid category id: " + categoryId);
274        // Should not reach here.
275        return 0;
276    }
277
278    public void setCurrentCategoryId(final int categoryId) {
279        mCurrentCategoryId = categoryId;
280        Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
281    }
282
283    public void setCurrentCategoryPageId(final int id) {
284        mCurrentCategoryPageId = id;
285    }
286
287    public int getCurrentCategoryPageId() {
288        return mCurrentCategoryPageId;
289    }
290
291    public void saveLastTypedCategoryPage() {
292        Settings.writeLastTypedEmojiCategoryPageId(
293                mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
294    }
295
296    public boolean isInRecentTab() {
297        return mCurrentCategoryId == EmojiCategory.ID_RECENTS;
298    }
299
300    public int getTabIdFromCategoryId(final int categoryId) {
301        for (int i = 0; i < mShownCategories.size(); ++i) {
302            if (mShownCategories.get(i).mCategoryId == categoryId) {
303                return i;
304            }
305        }
306        Log.w(TAG, "categoryId not found: " + categoryId);
307        return 0;
308    }
309
310    // Returns the view pager's page position for the categoryId
311    public int getPageIdFromCategoryId(final int categoryId) {
312        final int lastSavedCategoryPageId =
313                Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId);
314        int sum = 0;
315        for (int i = 0; i < mShownCategories.size(); ++i) {
316            final CategoryProperties props = mShownCategories.get(i);
317            if (props.mCategoryId == categoryId) {
318                return sum + lastSavedCategoryPageId;
319            }
320            sum += props.mPageCount;
321        }
322        Log.w(TAG, "categoryId not found: " + categoryId);
323        return 0;
324    }
325
326    public int getRecentTabId() {
327        return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
328    }
329
330    private int getCategoryPageCount(final int categoryId) {
331        final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
332        return (keyboard.getSortedKeys().size() - 1) / mMaxPageKeyCount + 1;
333    }
334
335    // Returns a pair of the category id and the category page id from the view pager's page
336    // position. The category page id is numbered in each category. And the view page position
337    // is the position of the current shown page in the view pager which contains all pages of
338    // all categories.
339    public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) {
340        int sum = 0;
341        for (final CategoryProperties properties : mShownCategories) {
342            final int temp = sum;
343            sum += properties.mPageCount;
344            if (sum > position) {
345                return new Pair<>(properties.mCategoryId, position - temp);
346            }
347        }
348        return null;
349    }
350
351    // Returns a keyboard from the view pager's page position.
352    public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) {
353        final Pair<Integer, Integer> categoryAndId =
354                getCategoryIdAndPageIdFromPagePosition(position);
355        if (categoryAndId != null) {
356            return getKeyboard(categoryAndId.first, categoryAndId.second);
357        }
358        return null;
359    }
360
361    private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
362        return (((long) categoryId) << Integer.SIZE) | id;
363    }
364
365    public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) {
366        synchronized (mCategoryKeyboardMap) {
367            final Long categoryKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id);
368            if (mCategoryKeyboardMap.containsKey(categoryKeyboardMapKey)) {
369                return mCategoryKeyboardMap.get(categoryKeyboardMapKey);
370            }
371
372            if (categoryId == EmojiCategory.ID_RECENTS) {
373                final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
374                        mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
375                        mMaxPageKeyCount, categoryId);
376                mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd);
377                return kbd;
378            }
379
380            final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
381            final Key[][] sortedKeys = sortKeysIntoPages(
382                    keyboard.getSortedKeys(), mMaxPageKeyCount);
383            for (int pageId = 0; pageId < sortedKeys.length; ++pageId) {
384                final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
385                        mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
386                        mMaxPageKeyCount, categoryId);
387                for (final Key emojiKey : sortedKeys[pageId]) {
388                    if (emojiKey == null) {
389                        break;
390                    }
391                    tempKeyboard.addKeyLast(emojiKey);
392                }
393                mCategoryKeyboardMap.put(
394                        getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard);
395            }
396            return mCategoryKeyboardMap.get(categoryKeyboardMapKey);
397        }
398    }
399
400    public int getTotalPageCountOfAllCategories() {
401        int sum = 0;
402        for (CategoryProperties properties : mShownCategories) {
403            sum += properties.mPageCount;
404        }
405        return sum;
406    }
407
408    private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() {
409        @Override
410        public int compare(final Key lhs, final Key rhs) {
411            final Rect lHitBox = lhs.getHitBox();
412            final Rect rHitBox = rhs.getHitBox();
413            if (lHitBox.top < rHitBox.top) {
414                return -1;
415            } else if (lHitBox.top > rHitBox.top) {
416                return 1;
417            }
418            if (lHitBox.left < rHitBox.left) {
419                return -1;
420            } else if (lHitBox.left > rHitBox.left) {
421                return 1;
422            }
423            if (lhs.getCode() == rhs.getCode()) {
424                return 0;
425            }
426            return lhs.getCode() < rhs.getCode() ? -1 : 1;
427        }
428    };
429
430    private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) {
431        final ArrayList<Key> keys = new ArrayList<>(inKeys);
432        Collections.sort(keys, EMOJI_KEY_COMPARATOR);
433        final int pageCount = (keys.size() - 1) / maxPageCount + 1;
434        final Key[][] retval = new Key[pageCount][maxPageCount];
435        for (int i = 0; i < keys.size(); ++i) {
436            retval[i / maxPageCount][i % maxPageCount] = keys.get(i);
437        }
438        return retval;
439    }
440
441    private static boolean canShowFlagEmoji() {
442        Paint paint = new Paint();
443        String switzerland = "\uD83C\uDDE8\uD83C\uDDED"; //  U+1F1E8 U+1F1ED Flag for Switzerland
444        try {
445            return paint.hasGlyph(switzerland);
446        } catch (NoSuchMethodError e) {
447            // Compare display width of single-codepoint emoji to width of flag emoji to determine
448            // whether flag is rendered as single glyph or two adjacent regional indicator symbols.
449            float flagWidth = paint.measureText(switzerland);
450            float standardWidth = paint.measureText("\uD83D\uDC27"); //  U+1F427 Penguin
451            return flagWidth < standardWidth * 1.25;
452            // This assumes that a valid glyph for the flag emoji must be less than 1.25 times
453            // the width of the penguin.
454        }
455    }
456
457    private static boolean canShowUnicodeEightEmoji() {
458        Paint paint = new Paint();
459        String cheese = "\uD83E\uDDC0"; //  U+1F9C0 Cheese wedge
460        try {
461            return paint.hasGlyph(cheese);
462        } catch (NoSuchMethodError e) {
463            float cheeseWidth = paint.measureText(cheese);
464            float tofuWidth = paint.measureText("\uFFFE");
465            return cheeseWidth > tofuWidth;
466            // This assumes that a valid glyph for the cheese wedge must be greater than the width
467            // of the noncharacter.
468        }
469    }
470}
471