/* * This is the source code of Telegram for Android v. 5.x.x. * It is licensed under GNU GPL v. 2 or later. * You should have received a copy of the license in this archive (see LICENSE). * * Copyright Nikolai Kudashov, 2013-2018. */ package org.telegram.messenger; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Spannable; import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.DynamicDrawableSpan; import android.text.style.ImageSpan; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.io.File; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import tw.nekomimi.nekogram.EmojiProvider; import tw.nekomimi.nekogram.NekoXConfig; import tw.nekomimi.nekogram.NekoConfig; public class Emoji { private static HashMap rects = new HashMap<>(); private static int drawImgSize; private static int bigImgSize; private static boolean inited = false; private static Paint placeholderPaint; private static int[] emojiCounts = new int[]{1906, 199, 123, 332, 128, 222, 292, 259}; private static Bitmap[][] emojiBmp = new Bitmap[8][]; private static boolean[][] loadingEmoji = new boolean[8][]; public static HashMap emojiUseHistory = new HashMap<>(); public static ArrayList recentEmoji = new ArrayList<>(); public static HashMap emojiColor = new HashMap<>(); private static boolean recentEmojiLoaded; private static Runnable invalidateUiRunnable = () -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.emojiLoaded); public static float emojiDrawingYOffset; public static boolean emojiDrawingUseAlpha = true; private final static int MAX_RECENT_EMOJI_COUNT = 48; static { drawImgSize = AndroidUtilities.dp(20); bigImgSize = AndroidUtilities.dp(AndroidUtilities.isTablet() ? 40 : 34); for (int a = 0; a < emojiBmp.length; a++) { emojiBmp[a] = new Bitmap[emojiCounts[a]]; loadingEmoji[a] = new boolean[emojiCounts[a]]; } for (int j = 0; j < EmojiData.data.length; j++) { int position; for (int i = 0; i < EmojiData.data[j].length; i++) { rects.put(EmojiData.data[j][i], new DrawableInfo((byte) j, (short) i, i)); } } placeholderPaint = new Paint(); placeholderPaint.setColor(0x00000000); } public static void preloadEmoji(CharSequence code) { final DrawableInfo info = getDrawableInfo(code); if (info != null) { loadEmoji(info.page, info.page2); } } private static void loadEmoji(final byte page, final short page2) { if (emojiBmp[page][page2] == null) { if (loadingEmoji[page][page2]) { return; } loadingEmoji[page][page2] = true; Utilities.globalQueue.postRunnable(() -> { loadEmojiInternal(page, page2); loadingEmoji[page][page2] = false; }); } } private static void loadEmojiInternal(final byte page, final short page2) { try { float scale; int imageResize = 1; if (AndroidUtilities.density <= 1.0f) { scale = 2.0f; imageResize = 2; } else if (AndroidUtilities.density <= 1.5f) { scale = 2.0f; } else if (AndroidUtilities.density <= 2.0f) { scale = 2.0f; } else { scale = 2.0f; } Bitmap bitmap = null; try { InputStream is; String entry = "emoji/" + String.format(Locale.US, "%d_%d.png", page, page2); if (NekoConfig.useCustomEmoji.Bool()) { entry = "custom_emoji/" + entry; is = new FileInputStream(new File(ApplicationLoader.applicationContext.getFilesDir(), entry)); } else { is = ApplicationLoader.applicationContext.getAssets().open(entry); } BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = false; opts.inSampleSize = imageResize; bitmap = BitmapFactory.decodeStream(is, null, opts); is.close(); } catch (Throwable e) { FileLog.e(e); } final Bitmap finalBitmap = bitmap; emojiBmp[page][page2] = finalBitmap; AndroidUtilities.cancelRunOnUIThread(invalidateUiRunnable); AndroidUtilities.runOnUIThread(invalidateUiRunnable); } catch (Throwable x) { if (BuildVars.LOGS_ENABLED) { FileLog.e("Error loading emoji", x); } } } public static void invalidateAll(View view) { if (view instanceof ViewGroup) { ViewGroup g = (ViewGroup) view; for (int i = 0; i < g.getChildCount(); i++) { invalidateAll(g.getChildAt(i)); } } else if (view instanceof TextView) { view.invalidate(); } } public static String fixEmoji(String emoji) { char ch; int length = emoji.length(); for (int a = 0; a < length; a++) { ch = emoji.charAt(a); if (ch >= 0xD83C && ch <= 0xD83E) { if (ch == 0xD83C && a < length - 1) { ch = emoji.charAt(a + 1); if (ch == 0xDE2F || ch == 0xDC04 || ch == 0xDE1A || ch == 0xDD7F || ch == 0xDFF3 || ch == 0xDF2B || ch == 0xDC41 || ch == 0xDD75 || ch == 0xDFCC || ch == 0xDFCB) { emoji = emoji.substring(0, a + 2) + "\uFE0F" + emoji.substring(a + 2); length++; a += 2; } else { a++; } } else { a++; } } else if (ch == 0x20E3) { return emoji; } else if (ch >= 0x0023 && ch <= 0x3299) { if (EmojiData.emojiToFE0FMap.containsKey(ch)) { emoji = emoji.substring(0, a + 1) + "\uFE0F" + emoji.substring(a + 1); length++; a++; } } } return emoji; } public static EmojiDrawable getEmojiDrawable(CharSequence code) { DrawableInfo info = getDrawableInfo(code); if (info == null) { return null; } EmojiDrawable ed = new EmojiDrawable(info); ed.setBounds(0, 0, drawImgSize, drawImgSize); return ed; } private static DrawableInfo getDrawableInfo(CharSequence code) { DrawableInfo info = rects.get(code); if (info == null) { CharSequence newCode = EmojiData.emojiAliasMap.get(code); if (newCode != null) { info = Emoji.rects.get(newCode); } } return info; } public static boolean isValidEmoji(CharSequence code) { if (TextUtils.isEmpty(code)) { return false; } DrawableInfo info = rects.get(code); if (info == null) { CharSequence newCode = EmojiData.emojiAliasMap.get(code); if (newCode != null) { info = Emoji.rects.get(newCode); } } return info != null; } public static Drawable getEmojiBigDrawable(String code) { EmojiDrawable ed = getEmojiDrawable(code); if (ed == null) { CharSequence newCode = EmojiData.emojiAliasMap.get(code); if (newCode != null) { ed = Emoji.getEmojiDrawable(newCode); } } if (ed == null) { return null; } ed.setBounds(0, 0, bigImgSize, bigImgSize); ed.fullSize = true; return ed; } public static class EmojiDrawable extends Drawable { private DrawableInfo info; private boolean fullSize = false; private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); private static final Rect rect = new Rect(); private static final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); public int placeholderColor = 0x20000000; public EmojiDrawable(DrawableInfo i) { info = i; } public DrawableInfo getDrawableInfo() { return info; } public Rect getDrawRect() { Rect original = getBounds(); int cX = original.centerX(), cY = original.centerY(); rect.left = cX - (fullSize ? bigImgSize : drawImgSize) / 2; rect.right = cX + (fullSize ? bigImgSize : drawImgSize) / 2; rect.top = cY - (fullSize ? bigImgSize : drawImgSize) / 2; rect.bottom = cY + (fullSize ? bigImgSize : drawImgSize) / 2; return rect; } @Override public void draw(Canvas canvas) { if (!isLoaded()) { loadEmoji(info.page, info.page2); placeholderPaint.setColor(placeholderColor); Rect bounds = getBounds(); canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() * .4f, placeholderPaint); return; } Rect b; if (fullSize) { b = getDrawRect(); } else { b = getBounds(); } if (!NekoConfig.useSystemEmoji.Bool() && EmojiProvider.containsEmoji) { if (!isLoaded()) { loadEmoji(info.page, info.page2); canvas.drawRect(getBounds(), placeholderPaint); } else if (!canvas.quickReject(b.left, b.top, b.right, b.bottom, Canvas.EdgeType.AA)) { canvas.drawBitmap(emojiBmp[info.page][info.page2], null, b, paint); } return; } String emoji = fixEmoji(EmojiData.data[info.page][info.emojiIndex]); if (!NekoConfig.useSystemEmoji.Bool() && EmojiProvider.isFont) { try { textPaint.setTypeface(EmojiProvider.getFont()); } catch (RuntimeException ignored) { } } else if (NekoConfig.useSystemEmoji.Bool()) { try { textPaint.setTypeface(NekoXConfig.getSystemEmojiTypeface()); } catch (RuntimeException ignored) { } } textPaint.setTextSize(b.height() * 0.8f); canvas.drawText(emoji, 0, emoji.length(), b.left, b.bottom - b.height() * 0.225f, textPaint); } @Override public int getOpacity() { return PixelFormat.TRANSPARENT; } @Override public void setAlpha(int alpha) { paint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { } public boolean isLoaded() { if (!EmojiProvider.containsEmoji || NekoConfig.useSystemEmoji.Bool()) { return true; } return emojiBmp[info.page][info.page2] != null; } public void preload() { if (!isLoaded()) { loadEmoji(info.page, info.page2); } } } private static class DrawableInfo { public byte page; public short page2; public int emojiIndex; public DrawableInfo(byte p, short p2, int index) { page = p; page2 = p2; emojiIndex = index; } } private static boolean inArray(char c, char[] a) { for (char cc : a) { if (cc == c) { return true; } } return false; } public static class EmojiSpanRange { public EmojiSpanRange(int start, int end, CharSequence code) { this.start = start; this.end = end; this.code = code; } int start; int end; CharSequence code; } public static boolean fullyConsistsOfEmojis(CharSequence cs) { int[] emojiOnly = new int[1]; parseEmojis(cs, emojiOnly); return emojiOnly[0] > 0; } public static ArrayList parseEmojis(CharSequence cs) { return parseEmojis(cs, null); } public static ArrayList parseEmojis(CharSequence cs, int[] emojiOnly) { ArrayList emojis = new ArrayList<>(); if (cs == null || cs.length() <= 0) { return emojis; } long buf = 0; char c; int startIndex = -1; int startLength = 0; int previousGoodIndex = 0; StringBuilder emojiCode = new StringBuilder(16); int length = cs.length(); boolean doneEmoji = false; boolean notOnlyEmoji; try { for (int i = 0; i < length; i++) { c = cs.charAt(i); notOnlyEmoji = false; if (c >= 0xD83C && c <= 0xD83E || (buf != 0 && (buf & 0xFFFFFFFF00000000L) == 0 && (buf & 0xFFFF) == 0xD83C && (c >= 0xDDE6 && c <= 0xDDFF))) { if (startIndex == -1) { startIndex = i; } emojiCode.append(c); startLength++; buf <<= 16; buf |= c; } else if (emojiCode.length() > 0 && (c == 0x2640 || c == 0x2642 || c == 0x2695)) { emojiCode.append(c); startLength++; buf = 0; doneEmoji = true; } else if (buf > 0 && (c & 0xF000) == 0xD000) { emojiCode.append(c); startLength++; buf = 0; doneEmoji = true; } else if (c == 0x20E3) { if (i > 0) { char c2 = cs.charAt(previousGoodIndex); if ((c2 >= '0' && c2 <= '9') || c2 == '#' || c2 == '*') { startIndex = previousGoodIndex; startLength = i - previousGoodIndex + 1; emojiCode.append(c2); emojiCode.append(c); doneEmoji = true; } } } else if ((c == 0x00A9 || c == 0x00AE || c >= 0x203C && c <= 0x3299) && EmojiData.dataCharsMap.containsKey(c)) { if (startIndex == -1) { startIndex = i; } startLength++; emojiCode.append(c); doneEmoji = true; } else if (startIndex != -1) { emojiCode.setLength(0); startIndex = -1; startLength = 0; doneEmoji = false; } else if (c != 0xfe0f) { notOnlyEmoji = true; } if (doneEmoji && i + 2 < length) { char next = cs.charAt(i + 1); if (next == 0xD83C) { next = cs.charAt(i + 2); if (next >= 0xDFFB && next <= 0xDFFF) { emojiCode.append(cs.subSequence(i + 1, i + 3)); startLength += 2; i += 2; } } else if (emojiCode.length() >= 2 && emojiCode.charAt(0) == 0xD83C && emojiCode.charAt(1) == 0xDFF4 && next == 0xDB40) { i++; while (true) { emojiCode.append(cs.subSequence(i, i + 2)); startLength += 2; i += 2; if (i >= cs.length() || cs.charAt(i) != 0xDB40) { i--; break; } } } } previousGoodIndex = i; char prevCh = c; for (int a = 0; a < 3; a++) { if (i + 1 < length) { c = cs.charAt(i + 1); if (a == 1) { if (c == 0x200D && emojiCode.length() > 0) { notOnlyEmoji = false; emojiCode.append(c); i++; startLength++; doneEmoji = false; } } else if (startIndex != -1 || prevCh == '*' || prevCh == '#' || prevCh >= '0' && prevCh <= '9') { if (c >= 0xFE00 && c <= 0xFE0F) { i++; startLength++; if (!doneEmoji) { doneEmoji = i + 1 >= length; } } } } } if (notOnlyEmoji && emojiOnly != null) { emojiOnly[0] = 0; emojiOnly = null; } if (doneEmoji && i + 2 < length && cs.charAt(i + 1) == 0xD83C) { char next = cs.charAt(i + 2); if (next >= 0xDFFB && next <= 0xDFFF) { emojiCode.append(cs.subSequence(i + 1, i + 3)); startLength += 2; i += 2; } } if (doneEmoji) { if (emojiOnly != null) { emojiOnly[0]++; } emojis.add(new EmojiSpanRange(startIndex, startIndex + startLength, emojiCode.subSequence(0, emojiCode.length()))); startLength = 0; startIndex = -1; emojiCode.setLength(0); doneEmoji = false; } } } catch (Exception e) { FileLog.e(e); } if (emojiOnly != null && emojiCode.length() != 0) { emojiOnly[0] = 0; } return emojis; } public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetricsInt fontMetrics, int size, boolean createNew) { return replaceEmoji(cs, fontMetrics, size, createNew, null, false, null); } public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetricsInt fontMetrics, int size, boolean createNew, boolean allowAnimated, AtomicReference> viewRef) { return replaceEmoji(cs, fontMetrics, size, createNew, null, allowAnimated, viewRef); } public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetricsInt fontMetrics, int size, boolean createNew, int[] emojiOnly) { return replaceEmoji(cs, fontMetrics, size, createNew, emojiOnly, false, null); } public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetricsInt fontMetrics, int size, boolean createNew, int[] emojiOnly, boolean allowAnimated, AtomicReference> viewRef) { allowAnimated = false; if ((NekoConfig.useSystemEmoji.Bool() || cs.length() == 0) && emojiOnly == null) { if (cs instanceof Spannable) { return cs; } return Spannable.Factory.getInstance().newSpannable(cs.toString()); } Spannable s; if (!createNew && cs instanceof Spannable) { s = (Spannable) cs; } else { s = Spannable.Factory.getInstance().newSpannable(cs.toString()); } ArrayList emojis = parseEmojis(s, emojiOnly); EmojiSpan span; Drawable drawable; for (int i = 0; i < emojis.size(); ++i) { EmojiSpanRange emojiRange = emojis.get(i); try { drawable = Emoji.getEmojiDrawable(emojiRange.code); if (drawable != null) { span = new EmojiSpan(drawable, DynamicDrawableSpan.ALIGN_BOTTOM, size, fontMetrics); s.setSpan(span, emojiRange.start, emojiRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } catch (Exception e) { FileLog.e(e); } if ((Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29) && !BuildVars.DEBUG_PRIVATE_VERSION && (i + 1) >= 50) { break; } } return s; } public static class EmojiSpan extends ImageSpan { private Paint.FontMetricsInt fontMetrics; private int size = AndroidUtilities.dp(20); public EmojiSpan(Drawable d, int verticalAlignment, int s, Paint.FontMetricsInt original) { super(d, verticalAlignment); fontMetrics = original; if (original != null) { size = Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent); if (size == 0) { size = AndroidUtilities.dp(20); } } } public void replaceFontMetrics(Paint.FontMetricsInt newMetrics, int newSize) { fontMetrics = newMetrics; size = newSize; } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { if (fm == null) { fm = new Paint.FontMetricsInt(); } if (fontMetrics == null) { int sz = super.getSize(paint, text, start, end, fm); int offset = AndroidUtilities.dp(8); int w = AndroidUtilities.dp(10); fm.top = -w - offset; fm.bottom = w - offset; fm.ascent = -w - offset; fm.leading = 0; fm.descent = w - offset; return sz; } else { if (fm != null) { fm.ascent = fontMetrics.ascent; fm.descent = fontMetrics.descent; fm.top = fontMetrics.top; fm.bottom = fontMetrics.bottom; } if (getDrawable() != null) { getDrawable().setBounds(0, 0, size, size); } return size; } } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { boolean restoreAlpha = false; if (paint.getAlpha() != 255 && emojiDrawingUseAlpha) { restoreAlpha = true; getDrawable().setAlpha(paint.getAlpha()); } boolean needRestore = false; if (emojiDrawingYOffset != 0) { needRestore = true; canvas.save(); canvas.translate(0, emojiDrawingYOffset); } super.draw(canvas, text, start, end, x, top, y, bottom, paint); if (needRestore) { canvas.restore(); } if (restoreAlpha) { getDrawable().setAlpha(255); } } @Override public void updateDrawState(TextPaint ds) { if (getDrawable() instanceof EmojiDrawable) { ((EmojiDrawable) getDrawable()).placeholderColor = 0x20ffffff & ds.getColor(); } super.updateDrawState(ds); } } public static void addRecentEmoji(String code) { Integer count = emojiUseHistory.get(code); if (count == null) { count = 0; } if (count == 0 && emojiUseHistory.size() >= MAX_RECENT_EMOJI_COUNT) { String emoji = recentEmoji.get(recentEmoji.size() - 1); emojiUseHistory.remove(emoji); recentEmoji.set(recentEmoji.size() - 1, code); } emojiUseHistory.put(code, ++count); } public static void sortEmoji() { recentEmoji.clear(); for (HashMap.Entry entry : emojiUseHistory.entrySet()) { recentEmoji.add(entry.getKey()); } Collections.sort(recentEmoji, (lhs, rhs) -> { Integer count1 = emojiUseHistory.get(lhs); Integer count2 = emojiUseHistory.get(rhs); if (count1 == null) { count1 = 0; } if (count2 == null) { count2 = 0; } if (count1 > count2) { return -1; } else if (count1 < count2) { return 1; } return 0; }); while (recentEmoji.size() > MAX_RECENT_EMOJI_COUNT) { recentEmoji.remove(recentEmoji.size() - 1); } } public static void saveRecentEmoji() { SharedPreferences preferences = MessagesController.getGlobalEmojiSettings(); StringBuilder stringBuilder = new StringBuilder(); for (HashMap.Entry entry : emojiUseHistory.entrySet()) { if (stringBuilder.length() != 0) { stringBuilder.append(","); } stringBuilder.append(entry.getKey()); stringBuilder.append("="); stringBuilder.append(entry.getValue()); } preferences.edit().putString("emojis2", stringBuilder.toString()).commit(); } public static void clearRecentEmoji() { SharedPreferences preferences = MessagesController.getGlobalEmojiSettings(); preferences.edit().putBoolean("filled_default", true).commit(); emojiUseHistory.clear(); recentEmoji.clear(); saveRecentEmoji(); } public static void loadRecentEmoji() { if (recentEmojiLoaded) { return; } recentEmojiLoaded = true; SharedPreferences preferences = MessagesController.getGlobalEmojiSettings(); String str; try { emojiUseHistory.clear(); if (preferences.contains("emojis")) { str = preferences.getString("emojis", ""); if (str != null && str.length() > 0) { String[] args = str.split(","); for (String arg : args) { String[] args2 = arg.split("="); long value = Utilities.parseLong(args2[0]); StringBuilder string = new StringBuilder(); for (int a = 0; a < 4; a++) { char ch = (char) value; string.insert(0, ch); value >>= 16; if (value == 0) { break; } } if (string.length() > 0) { emojiUseHistory.put(string.toString(), Utilities.parseInt(args2[1])); } } } preferences.edit().remove("emojis").commit(); saveRecentEmoji(); } else { str = preferences.getString("emojis2", ""); if (str != null && str.length() > 0) { String[] args = str.split(","); for (String arg : args) { String[] args2 = arg.split("="); emojiUseHistory.put(args2[0], Utilities.parseInt(args2[1])); } } } if (emojiUseHistory.isEmpty()) { if (!preferences.getBoolean("filled_default", false)) { String[] newRecent = new String[]{ "\uD83D\uDE02", "\uD83D\uDE18", "\u2764", "\uD83D\uDE0D", "\uD83D\uDE0A", "\uD83D\uDE01", "\uD83D\uDC4D", "\u263A", "\uD83D\uDE14", "\uD83D\uDE04", "\uD83D\uDE2D", "\uD83D\uDC8B", "\uD83D\uDE12", "\uD83D\uDE33", "\uD83D\uDE1C", "\uD83D\uDE48", "\uD83D\uDE09", "\uD83D\uDE03", "\uD83D\uDE22", "\uD83D\uDE1D", "\uD83D\uDE31", "\uD83D\uDE21", "\uD83D\uDE0F", "\uD83D\uDE1E", "\uD83D\uDE05", "\uD83D\uDE1A", "\uD83D\uDE4A", "\uD83D\uDE0C", "\uD83D\uDE00", "\uD83D\uDE0B", "\uD83D\uDE06", "\uD83D\uDC4C", "\uD83D\uDE10", "\uD83D\uDE15"}; for (int i = 0; i < newRecent.length; i++) { emojiUseHistory.put(newRecent[i], newRecent.length - i); } preferences.edit().putBoolean("filled_default", true).commit(); saveRecentEmoji(); } } sortEmoji(); } catch (Exception e) { FileLog.e(e); } try { str = preferences.getString("color", ""); if (str != null && str.length() > 0) { String[] args = str.split(","); for (int a = 0; a < args.length; a++) { String arg = args[a]; String[] args2 = arg.split("="); emojiColor.put(args2[0], args2[1]); } } } catch (Exception e) { FileLog.e(e); } } public static void saveEmojiColors() { SharedPreferences preferences = MessagesController.getGlobalEmojiSettings(); StringBuilder stringBuilder = new StringBuilder(); for (HashMap.Entry entry : emojiColor.entrySet()) { if (stringBuilder.length() != 0) { stringBuilder.append(","); } stringBuilder.append(entry.getKey()); stringBuilder.append("="); stringBuilder.append(entry.getValue()); } preferences.edit().putString("color", stringBuilder.toString()).commit(); } /** * NekoX: This function tries to fix incomplete emoji display shown in AvatarDrawable * In AvatarDrawable, only the first char is used to draw "avatar". * @return The first char or the first emoji */ public static String getFirstCharSafely(String source) { source = source.trim(); if (source.length() <= 1) return source; StringBuilder sb = new StringBuilder(); boolean nextEmoji = false; int code = source.codePointAt(0); sb.appendCodePoint(code); // append the first "char" for (int offset = code > 0xFFFF ? 2 : 1; offset < source.length(); offset++) { code = source.codePointAt(offset); if (code > 0xFFFF) offset++; if (nextEmoji || code == 0xFE0F || code == 0x20E3 || (code >= 0x1F3FB && code <= 0x1F3FF)) { // 0xFE0F: VARIATION SELECTOR-16, 20E3: Keycap, 0x1F3FB ~ 0x1F3FF: skin tone sb.appendCodePoint(code); nextEmoji = false; continue; } else if ((code >= 0x1F1E6 && code <= 0x1F1FF)) { sb.appendCodePoint(code); break; // 0x1F1E6 ~ 0x1F1FF: regional indicator symbol letter a to z // These unicode are also used in the first "char" of country flag emoji, so break immediately to prevent appending two emoji } else if (code == 0x200D) { // 0x200D: ZWJ sb.appendCodePoint(code); nextEmoji = true; continue; } if (!nextEmoji) break; } return sb.toString(); } }