Home | History | Annotate | Download | only in emoji
      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 
     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.Paint;
     23 import android.graphics.Rect;
     24 import android.os.Build;
     25 import android.util.Log;
     26 import android.util.Pair;
     27 
     28 import com.android.inputmethod.compat.BuildCompatUtils;
     29 import com.android.inputmethod.keyboard.Key;
     30 import com.android.inputmethod.keyboard.Keyboard;
     31 import com.android.inputmethod.keyboard.KeyboardId;
     32 import com.android.inputmethod.keyboard.KeyboardLayoutSet;
     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     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