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