| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.keyboard.emoji; |
| |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.os.Build; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import com.android.inputmethod.compat.BuildCompatUtils; |
| import com.android.inputmethod.keyboard.Key; |
| import com.android.inputmethod.keyboard.Keyboard; |
| import com.android.inputmethod.keyboard.KeyboardId; |
| import com.android.inputmethod.keyboard.KeyboardLayoutSet; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.settings.Settings; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| final class EmojiCategory { |
| private final String TAG = EmojiCategory.class.getSimpleName(); |
| |
| private static final int ID_UNSPECIFIED = -1; |
| public static final int ID_RECENTS = 0; |
| private static final int ID_PEOPLE = 1; |
| private static final int ID_OBJECTS = 2; |
| private static final int ID_NATURE = 3; |
| private static final int ID_PLACES = 4; |
| private static final int ID_SYMBOLS = 5; |
| private static final int ID_EMOTICONS = 6; |
| private static final int ID_FLAGS = 7; |
| private static final int ID_EIGHT_SMILEY_PEOPLE = 8; |
| private static final int ID_EIGHT_ANIMALS_NATURE = 9; |
| private static final int ID_EIGHT_FOOD_DRINK = 10; |
| private static final int ID_EIGHT_TRAVEL_PLACES = 11; |
| private static final int ID_EIGHT_ACTIVITY = 12; |
| private static final int ID_EIGHT_OBJECTS = 13; |
| private static final int ID_EIGHT_SYMBOLS = 14; |
| private static final int ID_EIGHT_FLAGS = 15; |
| private static final int ID_EIGHT_SMILEY_PEOPLE_BORING = 16; |
| |
| public final class CategoryProperties { |
| public final int mCategoryId; |
| public final int mPageCount; |
| public CategoryProperties(final int categoryId, final int pageCount) { |
| mCategoryId = categoryId; |
| mPageCount = pageCount; |
| } |
| } |
| |
| private static final String[] sCategoryName = { |
| "recents", |
| "people", |
| "objects", |
| "nature", |
| "places", |
| "symbols", |
| "emoticons", |
| "flags", |
| "smiley & people", |
| "animals & nature", |
| "food & drink", |
| "travel & places", |
| "activity", |
| "objects2", |
| "symbols2", |
| "flags2", |
| "smiley & people2" }; |
| |
| private static final int[] sCategoryTabIconAttr = { |
| R.styleable.EmojiPalettesView_iconEmojiRecentsTab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory1Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory2Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory3Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory4Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory5Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory6Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory7Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory8Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory9Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory10Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory11Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory12Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory13Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory14Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory15Tab, |
| R.styleable.EmojiPalettesView_iconEmojiCategory16Tab }; |
| |
| private static final int[] sAccessibilityDescriptionResourceIdsForCategories = { |
| R.string.spoken_descrption_emoji_category_recents, |
| R.string.spoken_descrption_emoji_category_people, |
| R.string.spoken_descrption_emoji_category_objects, |
| R.string.spoken_descrption_emoji_category_nature, |
| R.string.spoken_descrption_emoji_category_places, |
| R.string.spoken_descrption_emoji_category_symbols, |
| R.string.spoken_descrption_emoji_category_emoticons, |
| R.string.spoken_descrption_emoji_category_flags, |
| R.string.spoken_descrption_emoji_category_eight_smiley_people, |
| R.string.spoken_descrption_emoji_category_eight_animals_nature, |
| R.string.spoken_descrption_emoji_category_eight_food_drink, |
| R.string.spoken_descrption_emoji_category_eight_travel_places, |
| R.string.spoken_descrption_emoji_category_eight_activity, |
| R.string.spoken_descrption_emoji_category_objects, |
| R.string.spoken_descrption_emoji_category_symbols, |
| R.string.spoken_descrption_emoji_category_flags, |
| R.string.spoken_descrption_emoji_category_eight_smiley_people }; |
| |
| private static final int[] sCategoryElementId = { |
| KeyboardId.ELEMENT_EMOJI_RECENTS, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY1, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY2, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY3, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY4, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY5, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY6, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY7, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY8, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY9, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY10, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY11, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY12, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY13, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY14, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY15, |
| KeyboardId.ELEMENT_EMOJI_CATEGORY16 }; |
| |
| private final SharedPreferences mPrefs; |
| private final Resources mRes; |
| private final int mMaxPageKeyCount; |
| private final KeyboardLayoutSet mLayoutSet; |
| private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>(); |
| private final int[] mCategoryTabIconId = new int[sCategoryName.length]; |
| private final ArrayList<CategoryProperties> mShownCategories = new ArrayList<>(); |
| private final ConcurrentHashMap<Long, DynamicGridKeyboard> mCategoryKeyboardMap = |
| new ConcurrentHashMap<>(); |
| |
| private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED; |
| private int mCurrentCategoryPageId = 0; |
| |
| public EmojiCategory(final SharedPreferences prefs, final Resources res, |
| final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) { |
| mPrefs = prefs; |
| mRes = res; |
| mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count); |
| mLayoutSet = layoutSet; |
| for (int i = 0; i < sCategoryName.length; ++i) { |
| mCategoryNameToIdMap.put(sCategoryName[i], i); |
| mCategoryTabIconId[i] = emojiPaletteViewAttr.getResourceId( |
| sCategoryTabIconAttr[i], 0); |
| } |
| |
| int defaultCategoryId = EmojiCategory.ID_SYMBOLS; |
| addShownCategoryId(EmojiCategory.ID_RECENTS); |
| if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.KITKAT) { |
| if (canShowUnicodeEightEmoji()) { |
| defaultCategoryId = EmojiCategory.ID_EIGHT_SMILEY_PEOPLE; |
| addShownCategoryId(EmojiCategory.ID_EIGHT_SMILEY_PEOPLE); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_ANIMALS_NATURE); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_FOOD_DRINK); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_TRAVEL_PLACES); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_ACTIVITY); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_OBJECTS); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_SYMBOLS); |
| addShownCategoryId(EmojiCategory.ID_EIGHT_FLAGS); |
| } else { |
| defaultCategoryId = EmojiCategory.ID_PEOPLE; |
| addShownCategoryId(EmojiCategory.ID_PEOPLE); |
| addShownCategoryId(EmojiCategory.ID_OBJECTS); |
| addShownCategoryId(EmojiCategory.ID_NATURE); |
| addShownCategoryId(EmojiCategory.ID_PLACES); |
| addShownCategoryId(EmojiCategory.ID_SYMBOLS); |
| if (canShowFlagEmoji()) { |
| addShownCategoryId(EmojiCategory.ID_FLAGS); |
| } |
| } |
| } else { |
| addShownCategoryId(EmojiCategory.ID_SYMBOLS); |
| } |
| addShownCategoryId(EmojiCategory.ID_EMOTICONS); |
| |
| DynamicGridKeyboard recentsKbd = |
| getKeyboard(EmojiCategory.ID_RECENTS, 0 /* categoryPageId */); |
| recentsKbd.loadRecentKeys(mCategoryKeyboardMap.values()); |
| |
| mCurrentCategoryId = Settings.readLastShownEmojiCategoryId(mPrefs, defaultCategoryId); |
| Log.i(TAG, "Last Emoji category id is " + mCurrentCategoryId); |
| if (!isShownCategoryId(mCurrentCategoryId)) { |
| Log.i(TAG, "Last emoji category " + mCurrentCategoryId + |
| " is invalid, starting in " + defaultCategoryId); |
| mCurrentCategoryId = defaultCategoryId; |
| } else if (mCurrentCategoryId == EmojiCategory.ID_RECENTS && |
| recentsKbd.getSortedKeys().isEmpty()) { |
| Log.i(TAG, "No recent emojis found, starting in category " + defaultCategoryId); |
| mCurrentCategoryId = defaultCategoryId; |
| } |
| } |
| |
| private void addShownCategoryId(final int categoryId) { |
| // Load a keyboard of categoryId |
| getKeyboard(categoryId, 0 /* categoryPageId */); |
| final CategoryProperties properties = |
| new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); |
| mShownCategories.add(properties); |
| } |
| |
| private boolean isShownCategoryId(final int categoryId) { |
| for (final CategoryProperties prop : mShownCategories) { |
| if (prop.mCategoryId == categoryId) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public static String getCategoryName(final int categoryId, final int categoryPageId) { |
| return sCategoryName[categoryId] + "-" + categoryPageId; |
| } |
| |
| public int getCategoryId(final String name) { |
| final String[] strings = name.split("-"); |
| return mCategoryNameToIdMap.get(strings[0]); |
| } |
| |
| public int getCategoryTabIcon(final int categoryId) { |
| return mCategoryTabIconId[categoryId]; |
| } |
| |
| public String getAccessibilityDescription(final int categoryId) { |
| return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]); |
| } |
| |
| public ArrayList<CategoryProperties> getShownCategories() { |
| return mShownCategories; |
| } |
| |
| public int getCurrentCategoryId() { |
| return mCurrentCategoryId; |
| } |
| |
| public int getCurrentCategoryPageSize() { |
| return getCategoryPageSize(mCurrentCategoryId); |
| } |
| |
| public int getCategoryPageSize(final int categoryId) { |
| for (final CategoryProperties prop : mShownCategories) { |
| if (prop.mCategoryId == categoryId) { |
| return prop.mPageCount; |
| } |
| } |
| Log.w(TAG, "Invalid category id: " + categoryId); |
| // Should not reach here. |
| return 0; |
| } |
| |
| public void setCurrentCategoryId(final int categoryId) { |
| mCurrentCategoryId = categoryId; |
| Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId); |
| } |
| |
| public void setCurrentCategoryPageId(final int id) { |
| mCurrentCategoryPageId = id; |
| } |
| |
| public int getCurrentCategoryPageId() { |
| return mCurrentCategoryPageId; |
| } |
| |
| public void saveLastTypedCategoryPage() { |
| Settings.writeLastTypedEmojiCategoryPageId( |
| mPrefs, mCurrentCategoryId, mCurrentCategoryPageId); |
| } |
| |
| public boolean isInRecentTab() { |
| return mCurrentCategoryId == EmojiCategory.ID_RECENTS; |
| } |
| |
| public int getTabIdFromCategoryId(final int categoryId) { |
| for (int i = 0; i < mShownCategories.size(); ++i) { |
| if (mShownCategories.get(i).mCategoryId == categoryId) { |
| return i; |
| } |
| } |
| Log.w(TAG, "categoryId not found: " + categoryId); |
| return 0; |
| } |
| |
| // Returns the view pager's page position for the categoryId |
| public int getPageIdFromCategoryId(final int categoryId) { |
| final int lastSavedCategoryPageId = |
| Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId); |
| int sum = 0; |
| for (int i = 0; i < mShownCategories.size(); ++i) { |
| final CategoryProperties props = mShownCategories.get(i); |
| if (props.mCategoryId == categoryId) { |
| return sum + lastSavedCategoryPageId; |
| } |
| sum += props.mPageCount; |
| } |
| Log.w(TAG, "categoryId not found: " + categoryId); |
| return 0; |
| } |
| |
| public int getRecentTabId() { |
| return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS); |
| } |
| |
| private int getCategoryPageCount(final int categoryId) { |
| final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); |
| return (keyboard.getSortedKeys().size() - 1) / mMaxPageKeyCount + 1; |
| } |
| |
| // Returns a pair of the category id and the category page id from the view pager's page |
| // position. The category page id is numbered in each category. And the view page position |
| // is the position of the current shown page in the view pager which contains all pages of |
| // all categories. |
| public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) { |
| int sum = 0; |
| for (final CategoryProperties properties : mShownCategories) { |
| final int temp = sum; |
| sum += properties.mPageCount; |
| if (sum > position) { |
| return new Pair<>(properties.mCategoryId, position - temp); |
| } |
| } |
| return null; |
| } |
| |
| // Returns a keyboard from the view pager's page position. |
| public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) { |
| final Pair<Integer, Integer> categoryAndId = |
| getCategoryIdAndPageIdFromPagePosition(position); |
| if (categoryAndId != null) { |
| return getKeyboard(categoryAndId.first, categoryAndId.second); |
| } |
| return null; |
| } |
| |
| private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) { |
| return (((long) categoryId) << Integer.SIZE) | id; |
| } |
| |
| public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { |
| synchronized (mCategoryKeyboardMap) { |
| final Long categoryKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id); |
| if (mCategoryKeyboardMap.containsKey(categoryKeyboardMapKey)) { |
| return mCategoryKeyboardMap.get(categoryKeyboardMapKey); |
| } |
| |
| if (categoryId == EmojiCategory.ID_RECENTS) { |
| final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, |
| mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), |
| mMaxPageKeyCount, categoryId); |
| mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd); |
| return kbd; |
| } |
| |
| final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); |
| final Key[][] sortedKeys = sortKeysIntoPages( |
| keyboard.getSortedKeys(), mMaxPageKeyCount); |
| for (int pageId = 0; pageId < sortedKeys.length; ++pageId) { |
| final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, |
| mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), |
| mMaxPageKeyCount, categoryId); |
| for (final Key emojiKey : sortedKeys[pageId]) { |
| if (emojiKey == null) { |
| break; |
| } |
| tempKeyboard.addKeyLast(emojiKey); |
| } |
| mCategoryKeyboardMap.put( |
| getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard); |
| } |
| return mCategoryKeyboardMap.get(categoryKeyboardMapKey); |
| } |
| } |
| |
| public int getTotalPageCountOfAllCategories() { |
| int sum = 0; |
| for (CategoryProperties properties : mShownCategories) { |
| sum += properties.mPageCount; |
| } |
| return sum; |
| } |
| |
| private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() { |
| @Override |
| public int compare(final Key lhs, final Key rhs) { |
| final Rect lHitBox = lhs.getHitBox(); |
| final Rect rHitBox = rhs.getHitBox(); |
| if (lHitBox.top < rHitBox.top) { |
| return -1; |
| } else if (lHitBox.top > rHitBox.top) { |
| return 1; |
| } |
| if (lHitBox.left < rHitBox.left) { |
| return -1; |
| } else if (lHitBox.left > rHitBox.left) { |
| return 1; |
| } |
| if (lhs.getCode() == rhs.getCode()) { |
| return 0; |
| } |
| return lhs.getCode() < rhs.getCode() ? -1 : 1; |
| } |
| }; |
| |
| private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) { |
| final ArrayList<Key> keys = new ArrayList<>(inKeys); |
| Collections.sort(keys, EMOJI_KEY_COMPARATOR); |
| final int pageCount = (keys.size() - 1) / maxPageCount + 1; |
| final Key[][] retval = new Key[pageCount][maxPageCount]; |
| for (int i = 0; i < keys.size(); ++i) { |
| retval[i / maxPageCount][i % maxPageCount] = keys.get(i); |
| } |
| return retval; |
| } |
| |
| private static boolean canShowFlagEmoji() { |
| Paint paint = new Paint(); |
| String switzerland = "\uD83C\uDDE8\uD83C\uDDED"; // U+1F1E8 U+1F1ED Flag for Switzerland |
| try { |
| return paint.hasGlyph(switzerland); |
| } catch (NoSuchMethodError e) { |
| // Compare display width of single-codepoint emoji to width of flag emoji to determine |
| // whether flag is rendered as single glyph or two adjacent regional indicator symbols. |
| float flagWidth = paint.measureText(switzerland); |
| float standardWidth = paint.measureText("\uD83D\uDC27"); // U+1F427 Penguin |
| return flagWidth < standardWidth * 1.25; |
| // This assumes that a valid glyph for the flag emoji must be less than 1.25 times |
| // the width of the penguin. |
| } |
| } |
| |
| private static boolean canShowUnicodeEightEmoji() { |
| Paint paint = new Paint(); |
| String cheese = "\uD83E\uDDC0"; // U+1F9C0 Cheese wedge |
| try { |
| return paint.hasGlyph(cheese); |
| } catch (NoSuchMethodError e) { |
| float cheeseWidth = paint.measureText(cheese); |
| float tofuWidth = paint.measureText("\uFFFE"); |
| return cheeseWidth > tofuWidth; |
| // This assumes that a valid glyph for the cheese wedge must be greater than the width |
| // of the noncharacter. |
| } |
| } |
| } |