1 /* 2 * Copyright (C) 2021 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 androidx.emoji2.bundled; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotEquals; 21 import static org.junit.Assert.assertNull; 22 import static org.junit.Assert.assertThat; 23 import static org.junit.Assert.assertTrue; 24 import static org.mockito.ArgumentMatchers.anyFloat; 25 import static org.mockito.ArgumentMatchers.anyInt; 26 import static org.mockito.Mockito.any; 27 import static org.mockito.Mockito.doAnswer; 28 import static org.mockito.Mockito.mock; 29 30 import android.content.Context; 31 import android.content.res.AssetManager; 32 import android.graphics.Canvas; 33 import android.graphics.Paint; 34 import android.graphics.text.PositionedGlyphs; 35 import android.graphics.text.TextRunShaper; 36 import android.text.Spanned; 37 38 import androidx.annotation.GuardedBy; 39 import androidx.annotation.NonNull; 40 import androidx.emoji.text.EmojiCompat; 41 import androidx.emoji.text.EmojiSpan; 42 import androidx.emoji.text.MetadataRepo; 43 import androidx.emoji2.bundled.util.EmojiMatcher; 44 import androidx.emoji2.bundled.util.Emoji; 45 import androidx.emoji2.bundled.util.TestString; 46 import androidx.test.core.app.ApplicationProvider; 47 import androidx.test.filters.LargeTest; 48 import androidx.test.filters.SdkSuppress; 49 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 import org.junit.runners.Parameterized; 53 import org.mockito.invocation.InvocationOnMock; 54 import org.mockito.stubbing.Answer; 55 56 import java.io.BufferedReader; 57 import java.io.IOException; 58 import java.io.InputStream; 59 import java.io.InputStreamReader; 60 import java.util.ArrayList; 61 import java.util.Collection; 62 import java.util.List; 63 64 /** 65 * Reads raw/allemojis.txt which includes all the emojis known to human kind and tests that 66 * EmojiCompat creates EmojiSpans for each one of them. 67 */ 68 @LargeTest 69 @RunWith(Parameterized.class) 70 @SdkSuppress(minSdkVersion = 19) 71 public class AllEmojisTest { 72 /** 73 * String representation for a single emoji 74 */ 75 private final String mString; 76 77 /** 78 * Codepoints of emoji for better assert error message. 79 */ 80 private final String mCodepoints; 81 82 private static class TestConfig extends EmojiCompat.Config { TestConfig(String fontPath)83 TestConfig(String fontPath) { 84 super(new TestEmojiDataLoader(fontPath)); 85 setReplaceAll(true); 86 } 87 } 88 89 private static class TestEmojiDataLoader implements EmojiCompat.MetadataRepoLoader { 90 static final Object S_METADATA_REPO_LOCK = new Object(); 91 // keep a static instance to in order not to slow down the tests 92 @GuardedBy("sMetadataRepoLock") 93 static volatile MetadataRepo sMetadataRepo; 94 95 private final String mFontPath; 96 TestEmojiDataLoader(String fontPath)97 TestEmojiDataLoader(String fontPath) { 98 mFontPath = fontPath; 99 } 100 101 @Override load(@onNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback)102 public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) { 103 if (sMetadataRepo == null) { 104 synchronized (S_METADATA_REPO_LOCK) { 105 if (sMetadataRepo == null) { 106 try { 107 final Context context = ApplicationProvider.getApplicationContext(); 108 final AssetManager assetManager = context.getAssets(); 109 sMetadataRepo = MetadataRepo.create(assetManager, mFontPath); 110 } catch (Throwable e) { 111 loaderCallback.onFailed(e); 112 throw new RuntimeException(e); 113 } 114 } 115 } 116 } 117 118 loaderCallback.onLoaded(sMetadataRepo); 119 } 120 } 121 122 @Parameterized.Parameters(name = "Emoji Render Test: {1}") data()123 public static Collection<Object[]> data() throws IOException { 124 final Context context = ApplicationProvider.getApplicationContext(); 125 try (InputStream inputStream = context.getAssets().open("emojis.txt")) { 126 final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 127 final Collection<Object[]> data = new ArrayList<>(); 128 final StringBuilder stringBuilder = new StringBuilder(); 129 final StringBuilder codePointsBuilder = new StringBuilder(); 130 131 String s; 132 while ((s = reader.readLine()) != null) { 133 s = s.trim(); 134 // pass comments 135 if (s.isEmpty() || s.startsWith("#")) continue; 136 137 stringBuilder.setLength(0); 138 codePointsBuilder.setLength(0); 139 140 // emoji codepoints are space separated: i.e. 0x1f1e6 0x1f1e8 141 final String[] split = s.split(" "); 142 143 for (int index = 0; index < split.length; index++) { 144 final String part = split[index].trim(); 145 codePointsBuilder.append(part); 146 codePointsBuilder.append(","); 147 stringBuilder.append(Character.toChars(Integer.parseInt(part, 16))); 148 } 149 150 String string = stringBuilder.toString(); 151 String codePoints = codePointsBuilder.toString(); 152 153 // TODO(nona): Enable test case for flags. 154 if (!isFlagEmoji(string)) { 155 data.add(new Object[]{string, codePoints}); 156 } 157 } 158 159 return data; 160 } 161 162 } 163 AllEmojisTest(String string, String codepoints)164 public AllEmojisTest(String string, String codepoints) { 165 mString = string; 166 mCodepoints = codepoints; 167 168 // TODO(nona): Add full covered EmojiCompat font. 169 EmojiCompat.reset(new TestConfig("NotoColorEmojiCompat.ttf")); 170 } 171 172 @Test testEmoji()173 public void testEmoji() { 174 assertTrue("EmojiCompat should have emoji: " + mCodepoints, 175 EmojiCompat.get().hasEmojiGlyph(mString)); 176 assertEmojiCompatAddsEmoji(mString); 177 assertSpanCanRenderEmoji(mString); 178 } 179 assertSpanCanRenderEmoji(final String str)180 private void assertSpanCanRenderEmoji(final String str) { 181 final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString()); 182 final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class); 183 184 Canvas canvas = mock(Canvas.class); 185 186 List<PositionedGlyphs> result = new ArrayList<>(); 187 188 doAnswer(new Answer() { 189 @Override 190 public Object answer(InvocationOnMock invocation) throws Throwable { 191 char[] text = invocation.getArgument(0); 192 int index = invocation.getArgument(1); 193 int count = invocation.getArgument(2); 194 Paint paint = invocation.getArgument(5); 195 196 PositionedGlyphs glyphs = TextRunShaper.shapeTextRun( 197 text, index, count, index, count, 0, 0, false, paint 198 ); 199 result.add(glyphs); 200 return null; 201 }; 202 }).when(canvas) 203 .drawText(any(char[].class), anyInt(), anyInt(), anyFloat(), anyFloat(), 204 any(Paint.class)); 205 spans[0].draw(canvas, spanned, 0, spanned.length(), 0, 0, 0, 0, new Paint()); 206 207 assertEquals(1, result.size()); 208 PositionedGlyphs glyphs = result.get(0); 209 210 // All inputs are single emojis. Thus if multiple glyphs are generated, likely the emoji 211 // sequence is decomposed. 212 assertEquals(mCodepoints, 1, glyphs.glyphCount()); 213 assertNotEquals(mCodepoints, 0, glyphs.getGlyphId(0)); 214 // null file path means the glyph is NOT came from system font. 215 assertNull(mCodepoints, glyphs.getFont(0).getFile()); 216 } 217 isFlagEmoji(String str)218 private static boolean isFlagEmoji(String str) { 219 return str.codePoints().allMatch(cp -> 0x1F1E6 <= cp && cp <= 0x1F1FF); 220 } 221 assertEmojiCompatAddsEmoji(final String str)222 private void assertEmojiCompatAddsEmoji(final String str) { 223 TestString string = new TestString(str); 224 CharSequence sequence = EmojiCompat.get().process(string.toString()); 225 assertThat(sequence, EmojiMatcher.hasEmojiCount(1)); 226 assertThat(sequence, 227 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex())); 228 229 // case where Emoji is in the middle of string 230 string = new TestString(str).withPrefix().withSuffix(); 231 sequence = EmojiCompat.get().process(string.toString()); 232 assertThat(sequence, EmojiMatcher.hasEmojiCount(1)); 233 assertThat(sequence, 234 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex())); 235 236 // case where Emoji is at the end of string 237 string = new TestString(str).withSuffix(); 238 sequence = EmojiCompat.get().process(string.toString()); 239 assertThat(sequence, EmojiMatcher.hasEmojiCount(1)); 240 assertThat(sequence, 241 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex())); 242 } 243 244 } 245